Passing a form object as a vue prop
Setup
Let's say we have a parent component:
Now because we want to re-use this exact form on many places in our app we move it to a child component like this:
While this looks easy and does work, it breaks vue's one-way data flow:
When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.
The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn't obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.
Solution 1: just use it like this
So if you are fine with not following the best practice and you know what you are doing you can mutate an object prop. But we aware that it can, in some cases, cause reactivity issues.
If you are using eslint-plugin-vue's recommended rules you probably have vue/no-mutating-props screaming at you. There is a config option "shallowOnly" that disables warnings about modifying object props.
Solution 2: local object and watch
First we create our own, local, reactive form object, then we create two watchers. One for keeping our local object up-to-date if the parent form prop changes and one that emits an update event when anything in our local object is modified.
Modification of the prop is now done in the parent, not the child.
Object.assign
has a neat side-effect, it allows the form object to have methods and be more complex. For me this was the case when using the inertiajs form helper. I only had to modfiy the initial import so that my local object and the data emitted by the update event does not include the methods
...
//initial import
const data = reactive(form.data());
...
Solution 2.1
One more reason for the Object.assign
in the previous example is because assigning d
to form
would replace the reactive object, breaking reactivity (in our case it would not work at all because we defined it as a const). So a variation would be to use a nested property:
Solution 2.2
This can be modified further by using v-model
<template>
<form>
<input v-model="data.a">
<input v-model="data.b">
</form>
</template>
<script>
export default {
props: ["modelValue"],
emits: ["update:modelValue"],
setup(props, {emit}) {
//initial import
const data = reactive(props.modelValue);
//dataflow down
watch(props.modelValue, v => Object.assign(data, v))
//dataflow up
watch(data, v => emit("update:modelValue", v))
return {
data
}
}
}
</script>
Solution 3: lots of models
From what I could find online, vue's intended way is defining a model for each value.
What I don't like about this is that I have to define each value twice, once in the parent template and once in the child setup. Imagine doing this for a big form. Furthermore it seems like this only works when using the new script setup
syntax and doing it in the pre 3.4 syntax you would need a lot of code.