Closed10

Nuxt Composition API 備忘録

matsu7089matsu7089

プロジェクト作成 & インストール

> yarn create nuxt-app ${プロジェクト名}
> cd ${プロジェクト名}
> yarn add --dev @nuxtjs/composition-api
nuxt.config.js
{
  buildModules: [
    '@nuxtjs/composition-api/module', // 追加
  ],
}
> npx nuxt -v
@nuxt/cli v2.15.6
matsu7089matsu7089

defineComponent

defineComponent を利用してコンポーネントを作成する
data, methods やライフサイクルを setup() 内に書く

setup() で返した値を template 側で利用できる

サンプルプログラム

<template>
  <div>
    <h1>{{ text }}</h1>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const text = 'Hello, World!'

    return {
      text,
    }
  },
})
</script>

matsu7089matsu7089

ref

プリミティブ(Object 以外)の値をリアクティブにするときは ref を使う
値の参照(更新)には .value でアクセスする

const sample = ref('Hello, World!') // 引数に初期値を指定する
console.log(sample.value) // -> Hello, World!

サンプルプログラム

<template>
  <div>
    <h1>{{ text }}</h1>
    <button @click="changeText">button</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const text = ref('Hello, World!')

    const changeText = () => {
      text.value = 'Hello, Nuxt!'
    }

    return {
      text,
      changeText,
    }
  },
})
</script>

matsu7089matsu7089

reactive

Object の値をリアクティブにするときは reactive を使う

const sample = reactive({
  str: 'abc',
  num: 100,
})
console.log(sample.str) // -> 'abc'

reactive から一部のプロパティだけ取り出す場合は toRef()toRefs() を使う
toRefs() の方が楽に書ける

const str = toRef(sample, 'str') // toRef(reactive値, '取り出すキー')
// or
const { str } = toRefs(sample) // toRefs(reactive値)

サンプルプログラム

<template>
  <div>
    <h1>{{ state.text }}</h1>
    <p>{{ state.count }}</p>
    <button @click="changeText">button</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from '@nuxtjs/composition-api'

interface State {
  text: string
  count: number
}

export default defineComponent({
  setup() {
    const state = reactive<State>({
      text: 'Hello, World!',
      count: 0,
    })

    const count = toRef(state, 'count')
    // or
    const { count } = toRefs(state)

    const changeText = () => {
      state.text = 'Hello, Nuxt!'
      count.value++
    }

    return {
      state,
      changeText,
    }
  },
})
</script>

matsu7089matsu7089

ライフサイクル

ライフサイクルは onMounted のように onXXX が用意されている
onMounted(実行する処理)setup() 内に書く

同じライフサイクルを複数回呼び出すこともできる

サンプルプログラム

<template>
  <div>
    <ul v-for="(log, i) in state.logs" :key="i">
      <li>{{ log }}</li>
    </ul>
  </div>
</template>

<script lang="ts">
import {
  defineComponent,
  onBeforeMount,
  onMounted,
  reactive,
} from '@nuxtjs/composition-api'

interface State {
  logs: string[]
}

export default defineComponent({
  setup() {
    const state = reactive<State>({
      logs: [],
    })

    onBeforeMount(() => {
      state.logs.push('onBeforeMount')
    })
    onMounted(() => {
      state.logs.push('onMounted1')
    })
    onMounted(() => {
      state.logs.push('onMounted2')
    })

    return {
      state,
    }
  },
})
</script>

matsu7089matsu7089

computed, watch

ライフサイクルと同じように setup() 内に
computed(実行する処理), watch(監視対象, 変更時の処理 [, オプション]) を書く

サンプルプログラム

<template>
  <div>
    <div v-for="(name, i) in names" :key="i">
      <input v-model="checkedNames" :value="name" type="checkbox" />
      <label>{{ name }}</label>
    </div>
    <p>{{ text }}</p>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, ref, watch } from '@nuxtjs/composition-api'

export default defineComponent({
  setup() {
    const names = ['Jack', 'John', 'Mike']
    const checkedNames = ref<string[]>([])

    // computed版
    const text = computed(() => {
      return 'checkedNames: ' + checkedNames.value.join(', ')
    })

    // watch版
    const text = ref('')
    watch(
      checkedNames,
      (newValue, oldValue) => {
        text.value = 'checkedNames: ' + newValue.join(', ')
      },
      { immediate: true, deep: false }
    )

    return {
      names,
      checkedNames,
      text,
    }
  },
})
</script>

matsu7089matsu7089

props, context

setup(props, context) のように setup() から受け取れる
props の値を分割代入するときは toRefs() を使う

context からは attrs, slots, emit にアクセスできる
setup(props, { attrs, slots, emit }) のように分割もできる

サンプルプログラム

Parent.vue
<template>
  <div>
    <h2>Parent</h2>
    <div v-for="(name, i) in names" :key="i">
      <input v-model="selectedName" :value="name" type="radio" />
      <label>{{ name }}</label>
    </div>
    <child :name="selectedName" @reset="onReset"></child>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from '@nuxtjs/composition-api'
import Child from '~/components/Child.vue'

export default defineComponent({
  components: { Child },
  setup() {
    const names = ['Jack', 'John', 'Mike']
    const selectedName = ref('')

    const onReset = () => {
      selectedName.value = ''
    }

    return {
      names,
      selectedName,
      onReset,
    }
  },
})
</script>
Child.vue
<template>
  <div>
    <h2>Child</h2>
    <p>{{ text }}</p>
    <button @click="reset">Reset</button>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, toRefs } from '@nuxtjs/composition-api'

export default defineComponent({
  props: {
    name: {
      type: String,
      default: '',
    },
  },
  emits: ['reset'],
  setup(props, { emit }) {
    const { name } = toRefs(props)

    const text = computed(() => {
      return name.value.toUpperCase()
    })

    const reset = () => {
      emit('reset')
    }

    return {
      text,
      reset,
    }
  },
})
</script>

matsu7089matsu7089

composable

setup() 内のロジックを別ファイルに分離できる

サンプルプログラム

useTheme.ts
import { computed, ref } from '@nuxtjs/composition-api'

/** 反対色を返す関数 */
const getOpposite = (color: string) => {
  return (
    '#' +
    color
      .match(/^#(.{2})(.{2})(.{2})$/)!
      .slice(1, 4)
      .map((v) => 255 - parseInt(v, 16))
      .map((v) => ('0' + v.toString(16)).slice(-2))
      .join('')
  )
}

export default function useTheme() {
  const mainColor = ref('#0080ff')
  const accentColor = computed(() => {
    return getOpposite(mainColor.value)
  })

  return {
    mainColor,
    accentColor,
  }
}
index.vue
<template>
  <div>
    <input type="color" v-model="mainColor" />
    <p v-bind:style="{ color: mainColor }">main: {{ mainColor }}</p>
    <p v-bind:style="{ color: accentColor }">accent: {{ accentColor }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useTheme from '~/composable/useTheme'

export default defineComponent({
  setup() {
    const { mainColor, accentColor } = useTheme()

    return {
      mainColor,
      accentColor,
    }
  },
})
</script>

matsu7089matsu7089

provide, inject

provide, inject を使うと親子関係のコンポーネント間で値(state など)を共有できる
子孫コンポーネントでも親の値を受け取れるので props バケツリレーを回避できる

サンプルプログラム

useTheme.ts
  export default function useTheme() { ... }

+ export type themeType = ReturnType<typeof useTheme>
+ export const themeKey: InjectionKey<themeType> = Symbol('themeKey')
Parent.vue
<template>
  <div>
    <input type="color" v-model="mainColor" />
    <p v-bind:style="{ color: mainColor }">main: {{ mainColor }}</p>
    <p v-bind:style="{ color: accentColor }">accent: {{ accentColor }}</p>
    <child></child>
  </div>
</template>

<script lang="ts">
import { defineComponent, provide } from '@nuxtjs/composition-api'
import useTheme, { themeKey } from '~/composable/useTheme'
import Child from '~/components/Child.vue'

export default defineComponent({
  components: { Child },
  setup() {
    const theme = useTheme()
    // provide
    provide(themeKey, theme)

    const { mainColor, accentColor } = theme
    
    return {
      mainColor,
      accentColor,
    }
  },
})
</script>
Child.vue
<template>
  <div>
    <h2>Child</h2>
    <p v-bind:style="{ color: mainColor }">main: {{ mainColor }}</p>
    <p v-bind:style="{ color: accentColor }">accent: {{ accentColor }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, inject } from '@nuxtjs/composition-api'
import { themeKey } from '~/composable/useTheme'

export default defineComponent({
  setup() {
    // inject
    const { mainColor, accentColor } = inject(themeKey)!

    return {
      mainColor,
      accentColor,
    }
  },
})
</script>

このスクラップは2021/11/20にクローズされました