1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

reactiveな変数を更新後に参照するwatchを絶対にトリガーしたい

Last updated at Posted at 2024-10-18

概要

  • タイトル通りのことをやろうとしてハマったのでメモする
  • 正しい実装かわからんので誰か教えてほしい

やりたかったこと

  • ComponentA、ComponentBがいて、ComponentAのアクションをComponentBに反映したかった
    • 具体はComponentAでボタンを押したら、ComponentBのフォームの内容を変更するという処理
  • 割と階層が離れている → provide/injectを利用した
    • Composableを用意して、reactiveを管理するstateとupdate関数を用意して、provideする
    • stateはreadonlyで返した
  • さっと作ったら問題があった
    • 例えばComponentAでアクションしてComponentBのフォームを更新した場合、ComponentBはreadonlyなreactive変数を受け取るので、そのままv-modelにわたすと、変更できないフォームが完成する
    • それは困るので、ComponentBでは、Composableからreadonlyの変数を受取り、watchするようにして、トリガーされたら、ComponentBのreactiveな変数を更新するようにした
      • これはよかったが、同様にComponentAで操作後、ComponentBのフォームを更新した後、ComponentBのフォームを手動で書き換えると、ComponentAでもっかい同じボタンを推押すと、AのState自体は変更がないため、伝播せずwatchがトリガーされない。ということが発生した

コードはこんな感じ

useFormState.ts
interface FormState {
  inputValue: Readonly<Ref<string>>
  updateInputValue: (value: string) => void
}

export const FormStateSymbol: InjectionKey<FormState> = Symbol('FormState')

/**
 * provide
 */
export function provideFormState () {
  const inputValue = ref<string>('')

  const updateInputValue = (value: string): void => {
    inputValue.value = value
  }

  const formState = {
    inputValue: readonly(inputValue),
    updateInputValue
  }

  provide(FormStateSymbol, formState)

  return formState
}

/**
 * inject
 */
export function useFormState (): FormState {
  const formState = inject(FormStateSymbol)
  if (!formState) {
    throw new Error('Form state not provided')
  }
  return formState
}
ComponentA.vue
<template>
    <button :click="setInputValue('hogehoge')">ボタン</button>
</template>

<script setup>
const { updateInputValue } = useFormState()

const setInputValue = (inputValue: string) => {
  updateInputValue(inputValue)
}
</script>
ComponentB.vue
<template>
      <input
        type="text"
        v-model="inputValue"
        placeholder="ほげほげ"
      >
</template>

<script setup>
const inputValue = ref('')
const { inputValue: otherInputValue } = useFormState()

// ここが動かないので困っている
watch(otherInputValue, (value: string) => {
  inputValue.value = value
})

</script>

ComponentAとComponentBの親Componentでprovideしています

provideFormState()

バージョン等

  • Vue3.5
  • Nuxt3.13.1

結論どうしたか

泣く泣く 別のreactive変数を用意して、そいつは必ず変更するようにしました

useFormState.ts
interface FormState {
  inputValue: Readonly<Ref<string>>
+ inputValueChanged: Readonly<Ref<number>>
  updateInputValue: (value: string) => void
}

export const FormStateSymbol: InjectionKey<FormState> = Symbol('FormState')

/**
 * provide
 */
export function provideFormState () {
  const inputValue = ref<string>('')
+ const inputValueChanged = ref<number>(0)

  const updateInputValue = (value: string): void => {
    inputValue.value = value
+   inputValueChanged.value++
  }

  const formState = {
    inputValue: readonly(inputValue),
    updateInputValue
  }

  provide(FormStateSymbol, formState)

  return formState
}

/**
 * inject
 */
export function useFormState (): FormState {
  const formState = inject(FormStateSymbol)
  if (!formState) {
    throw new Error('Form state not provided')
  }
  return formState
}
ComponentB.vue
<script setup>
+ const { inputValue: otherInputValue, inputValueChanged } = useFormState()

// inputValueChangedが必ず更新されるのでトリガーされる
+ watch([otherInputValue, inputValueChanged], ([value, _]: [string, number]) => {
  inputValue.value = value
})
</script>

色々試したこと

雑感

本当にこれでよいかわからないので誰か教えてください!(他力本願)

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?