概要
- タイトル通りのことをやろうとしてハマったのでメモする
- 正しい実装かわからんので誰か教えてほしい
やりたかったこと
- 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>
色々試したこと
- triggerRef
- https://ja.vuejs.org/api/reactivity-advanced#triggerref
- よくわからないが動作しなかった
- CustomRef
- これもだめ
-
watch(() => xxx.value, { flush: 'sync' })
とかにしてみる- これもだめ
雑感
本当にこれでよいかわからないので誰か教えてください!(他力本願)