Vue.js: Props as initial data

April 22, 2020
You're reading a draft of an essay, it's not published yet.

A component takes inputs as props, and produce outputs as events.

Props are one-way data flow, from parent to child. A child component should never change the props value.

The official docs of Vue.js states that props exist to give the component initial data from its parent.

I’ll take a Task Form component as our example, that will be used to create new tasks and edit existed one. This will demonstrate how to use props as the initial data

Why it’s initial data? Because each components should be totally isolated from the other components, and its state should never affect the outer state.

When we give the TaskForm a the task as its initial data, we’re telling it to be free managing the state of the component as its job requires.

But the task is not only passed to the component, it is cloned. This is important not to produce side effect, in case the parent changes the props value, it will be affects in the child component. To prevent that, we clone the object.

But sometimes this a feature, we need the child components’ props to always be updated, right?

So here’s the thing: we can split the components into two categories:

Read-only component

  • Only view the data
  • Doesn’t produce any side effects (doesn’t change the state of the data)

If your component doesn’t modify the passed props – you shouldn’t pass the cloned object, but the object itself.

Read-and-write component

  • View data and modify it.

If your components should view the data and has its own version of it to be modified, then the cloned object should be passed.

This usually happen in the components that have a form or inputs in it. It needs to have its own local version of the object to modify it without affecting the original object. Once it’s done with the cloned data, it can tell the parent to update its state.

Let’s have an example: update task form component.

	<template>
		<div>
			<input v-model="task.title" placeholder="Title">
			<textarea v-model="task.desceiption" placeholder="Desceiption"></textarea>
			<button @click="update">update</button>
		</div>
	</template>

	<script>
	export default {
		props: {
			initialTasl: {
				type: Object,
				required: true,
				default: {}
			},
		},

		data () {
			return {
				task: {...this.initialTask} // clone tht object
			}
		},

		mounted() {
			update() {
				// Update the task...
				// ...
				this.$emit('updated', this.task)
			}
		},
	}
	</script>

What are the inputs of this component? A single Task object What are the outputs (events fired)? @updated events

Now, when this component is used in, we pass into it the task object, then it will clone its own local version the object.

Any modifications on this version won’t affect the parent components. Once the users update the task and hit Update, it will fire an event telling the parent component to update the task.

Deep copy v.s. shallow copy

Cloning a local version of the prop may work by convenient spread operator {…object}. But in case your prop has nested array or objects, any change occurs on them will cause a side effects.

In the previous example, if the task model had nested properties.. let’s say it has actions array like this:

{
	"title": "Drink water.",
	"description": "Because it's so good",
	"actions": [
		{ "title": "Bring an empty glass" },
		{ "title": "Fill it with cold water" },
		{ "title": "Enjoy" },
	]
}

If we were meant to change the title of any of the actions above from the previous TaskForm component (which clones the object with object separator), it will log an error in the console (because you are trying to change the props in the child component).

This is because it’s shallow copied, the nested properties still points to the original object.

What are the proposed solutions?