[WIP] 違いでわかるSvelte

※この記事は書きかけです。すでに書いてある文章も変更になる可能性があります。

みんな大好きReact/Vueと比較してSvelteではどうやるの?でSvelteのことをわかった気になるための記事です。

はじめに

公式サイト 

現在のSvelteは.svelteファイルに script/style はタグで囲みつつ、template部分は空いているところに直接書けば動きます。
ちなみにroot elementは一つじゃなくてもちゃんと動きます。

<script lang="ts"// lang属性が使える
  const hoge = 'example'
</script>

<h1>hello</h1>
<div class="hoge">{hoge}</div>

<style>
  .hoge { color: red; }
</style>

Reactはhooks、Vueはv3のComposition APIで紹介していきます。

Component local states

React

import { useState } from 'react'

export function Counter({
  const [count] = useState(0)
  return <div>{count}</div>
}

Vue

<template>
  <div>{{count}}</div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}
</script>

Svelte

<script>
  let count = 0
</script>

<div>{count}</div>

Computed states

React

const [count] = useState(0)
const doubleCount = useMemo(() => count * 2, [count])

Vue

const count = ref(0)
const doubleCount = computed(() => count.value * 2)

Svelte

const count = 0
$: doubleCount = count * 2

Reference

React

const divRef = useRef(undefined)

useEffect(() => {
  if (divRef.current !== undefined) {
    ...
  }
})

<div ref={divRef}>hoge</div>

Vue

<template>
  <div ref="divRef">hoge</div>
</template>

...

const divRef = ref(undefined)

onMounted(() => {
  if (divRef.value !== undefined) {
    ...
  }
})

return { divRef }

Svelte

<script>
  import { onMount } from 'svelte'

  let divRef = undefined

  onMount(() => {
    if (divRef !== undefined) {
      ...
    }
  })  
</script>

<div bind:this={divRef}>hoge</div>

Events (native)

React

const handleClick = () => {
  ...
}

<button onClick={handleClick}>hoge</button>

Vue

<template>
  <button @click="handleClick">hoge</button>
</template>

...

const handleClick = () => {
  ...
}

return { handleClick }

Svelte

<script>
  const handleClick = () => {
    ...
  }
</script>

<button on:click={handleClick}>hoge</button>

Events (custom)

React

// Listener side
const handleCustomEvent = useCallback(({ hello }) => {
  ...
}, [])
<ExampleComponent onCustomEvent={handleCustomEvent}>

// Dispatcher side
props.onCustomEvent({ hello'world' })

Vue

// Listener side
function handleCustomEvent({ hello }{
  ...
}
<example-component @custom-event="handleCustomEvent">

// Dispatcher side
setup(props, { emit }) {
  emit('custom-event', { hello'world' })
}

Svelte

// Listener side
<ExampleComponent on:customEvent={handleCustomEvent}>

// Dispatcher side
import { createEventDispatcher } from 'svelte'

const dispatchCustomEvent = createEventDispatcher<{ customEvent: { hello: string } }>()
dispatchCustomEvent('customEvent', { hello: 'world' })

If condition

React

const showHello = true

{showHello && <div>hello</div>}

Vue

<template>
  <div v-if="showHello">hello</div>
</template>

...

const showHello = true
return { showHello }

Svelte

<script>
  const showHello = true
</script>

{#if showHello}
  <div>hello</div>
{/if}

Loop

React

const nums = [123]

return (
  <>
    {nums.map((num) => (
      <div key={num}>{num}</div>
    ))}
  </>
)

Vue

<template>
  <div v-for="num in nums" :key="num">{{num}}</div>
</template>

...

const nums = [123]
return { nums }

Svelte

<script>
  const nums = [123]
</script>

{#each nums as num (num)} // ()内がkeyになる
  <div>{num}</div>
{/each}

Props

React

type Props = {
  hello: string
}
export function ExampleComponent(props: Props{
  ...
}

Vue

import type { PropType } from 'vue'

props: {
  hello: {
    typeString as PropType<string>,
    required: true
  }
}

Svelte

export let hello: string

Slots

React

import type { ReactNode } from 'react'

type Props = {
  children: ReactNode
}
export function ExampleComponent(props: Props{
  return <div>{props.children}</div>
}

Vue

<template>
  <div>
    <slot />
  </div>
</template>

Svelte

<div>
  <slot />
</div>

Named Slots

React

import type { ReactNode } from ‘react’

type Props = {
  titleSlot: ReactNode
}
export function ExampleComponent(props: Props{
  return <div>{props.titleSlot}</div>
}

Vue

<template>
  <div>
    <slot name="title" />
  </div>
</template>

Svelte

<div>
  <slot name="title" />
</div>

Class (attribute)

React

const className = [
  'hello',
  props.active && 'is-active'
].map(Boolean).join(' ')

<div className={className}></div>

Vue

<div class="{ hello: true, 'is-active': active }"></div>

Svelte

<div class="hello" class:hello={active}></div>

Watch

React

const [count, setCount] = useState(0)

useEffect(() => {
  if (count !== 0 && count % 3 === 0) {
    console.log('A multiple of 3')
  }
}, [count])

Vue

const count = ref(0)
watch(count, (count) => {
  if (count !== 0 && count % 3 === 0) {
    console.log('A multiple of 3')
  }
})

Svelte

let count = 0

$: {
  if (count !== 0 && count % 3 === 0) {
    console.log('A multiple of 3')
  }
}

🚨HTML interpolation

React

<div dangerouslySetInnerHTML={{ __html'<p>Dager text</p>' }} />

Vue

<div v-html="<p>Danger text</p>" />

Svelte

{@html '<p>Danger text</p>'}

Lifecycles

React

useEffect(() => {
  // when a component is mounted

  return () => {
    // when a component is destroyed
  }
}, [])

useEffect(() => {
  // when props.something is updated
}, [props.something])

useEffect(() => {
  window.requestAnimationFrame(() => {
    // next tick
  })
}, [])

Vue

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated
  onBeforeUnmount
  onUnmounted
  onErrorCaptured
  onRenderTracked
  onRenderTriggered
  onActivated
  onDeactivated,
  nextTick
} from 'vue

Svelte

import {
  onMount,
  beforeUpdate,
  afterUpdate,
  onDestroy,
  tick
} from 'svelte'

Await/Suspense

TODO

Teleport

TODO