The Prop concept plays an important role in forming our awesome framework - React. It is one of the main methods of communication between components, bypassing it down, strengthens the relationship between parent and child components. Not only used to describe components, it sometimes seeds data to form initial State, another wonderful usage, yet controversial because there are hidden risks, this article aims to point them out and let's see how we can benefit from understanding them.
Table of Contents:
-
What is a Derived State? Why do we need it?
-
Two sources of truth: what problem it brings, and solution?
-
Accidentally mutate props: unpredictable bugs
-
Summary
What is a Derived State? Why do we need it?
When a prop is used to initiate a state, that state is called a derived state. We can see the prop as a data dependency, which the state uses to form its default value. Like my SetContainer the component has a state named sets and I use props.sets to initial sets.
Why we don't use props.sets it directly? Why do we have to map it to another variable (sets state) to use?
Great question! Short answer: temporary mutation.
Imagine we have a list, let's say a list of fitness exercises, each exercise has its own detail, like many sets with each set containing different duration, tempo, rest time. And users can pick an exercise and choose to edit the detail, after their edit, the exercise detail has to go back to its original sets detail. So the whole process is definitely like a temporary mutation on sets. We mutate the sets and render it out, once we did, we put it back. We need SetContainer to get the original sets, and only change the sets inside their scope, without affecting the original sets. The original sets here is props.sets, and the sets we gonna mutate is sets. Doing this way, we ensure the ability to change by setting state and it won't affect the original sets. In the example gif, I edited a value from 15 to 20, and when I got out of edit mode, it's back to 15, don't mind the language in the app lol.
As you can see, even Derived State is called anti-pattern, sometimes we need it. Personally, I think an anti-pattern is not bad at all, it is not perfect, it contains hidden risks, but if we are aware of those risks, it's still a good approach anyway! My article is not about criticizing Derived State, but about understanding the risks and conquering them!
Two Source of Truth
Who doesn't love Conan?
Shout out for Conan lol 💁
As we learn React, we must have come across the **Single source of truth **somewhere, like in Redux principle, or seeing other developers discuss it on Twitter, even many blog posts mention it.
For me,** a Single source of truth** is often misunderstood, it doesn't mean all things come from the same place, it means for every particular piece of data, there is one particular place where it comes from. This makes easy tracking of data flow, easing the frustration of programming.
By deriving state from props, you split the source of truth for one thing into two. It was originally a single thing (prop), derived state lets it diverge and potentially get out of sync, leading to problems like the below issue I found on StackOverflow. React.useState does not reload state from props *Functional components where we use useState to set initial values to our variable, if we pass initial value through…*stackoverflow.com
Briefly explain the issue, he uses a prop to the initial state, and when the prop changes, he expects the state would update like how it's derived from the prop, but it doesn't. His code:
Inside the function component, almost everything inside the function re-runs every it called (re-render), it's the function behavior of javascript. The useState the hook is a bit special because its creation isn't re-executed, much like setting state in the constructor for a class component. This leads to the issue above, “useState does not reload state from props”.
Writing this way, his component is likely to lose the future update from props.user, his data flow is now split into 2 sources, and as we see here, they are out of sync.
In the issue thread, others give him the solution of useEffect:
This works, unlike useState, useEffect re-executed after every render, as long as it meets the conditional dependency in the second argument. Now he has a way to sync his prop and state, every time his props.user updated, he sets a new value for his user state, but another problem appears, let me show you.
Like I mention about the temporary mutation above, this Avatar component doesn't need a derived state if its only purpose is to display the avatar. It gotta be something like editing avatar, for making more sense.
Saying we are doing the editing avatar feature, we let the user choose their profile picture as many times as they want, we don't wanna update props.user data or send update requests to the server immediately, it wastes. We want to make the actual update on the final decision of the user like he chooses his profile 5 times and on the 5th chosen, we send the update request to our server and locally update props.user. In my opinion, I just need, don't need the whole props.user data, but anyway I just stick to the StackOverflow issue above.
While choosing/editing the avatar, props.user suddenly changed, and useEffect syncs user state and props.user. By setting a new state, we lost our temporary mutation, our editing. We gotta fix it by caching our editing and doing conditional setUser in useEffect.
Imagining our data flow is way more complex, two sources of truth would lead to a bigger problem, and really hard to track.
However, it's not an anti-pattern if you make it clear that the prop is only to seed data for the component's internally-controlled state.
Derived state is sparingly used, for the best, only use it when your component's internal states don't need to sync with props from a parent, or even when you need a sync, remember don't do unconditional state update, always check your logic, find out if it needs any condition.
Accidentally mutate props
Every framework has its own ecosystem and set of rules. These rules are set to ensure developers don't break the vision/ecosystem of the framework. So does React, the update process of it is mainly asynchronous. From the beginning, we are taught not to mutate state/props directly, the process must be asynchronous, and React educates it by teaching us to use this.setState, or dispatch React*.*SetStateAction (2nd value of the returned tuple from useState), I did explain to them clearly why it is asynchronous? in my previous blog post, and Redux also gives this as one of the main rules in their documentation. These emphasize the importance of not mutating state/props directly, a must-follow rule.
That being said, everyone at least makes mistakes once, it could be a lack of knowledge, or carelessness, or lack of support from programming tools. Resulting in logical bugs when your app doesn't work as expected, or your components do not render correctly. Let's find out why!
For example, our Greeting component, let's say it can display some text like “Hello 31/12/2021” and uses props.date to initial dateState — derived state. In updateYear function, we want to update the year of dateState. Usually, we make changes to copy of object so that it won't affect the object, but sometimes, in a rush, we forgot and do something like :
const newDate = dateState
newDate.year = newYear
This affects to props.date, mutating it directly, and because today, newDate, dateState, props.date have the same reference, we accidentally change today. Let's say, I have other components used today for some calculation, after one more update batch, the calculation suddenly generates the wrong result, without experience, newbies would struggle to find out.
So remember to make changes on a copy of an object while working with a derived state, and be careful choosing how to make a copy, there are multiple choices, the decision between deep copy and shallow copy in Javascript.
Summary
Derived State is an anti-pattern, yet it's still a good approach when we need temporary mutation on our data, and for this case, I assume it's the most simple approach out there.
Two sources of truth are hard to manage than a single source of truth, of course. It's the most ideal to use derived state when your component won't care about the update from parent's props (the props used for derived state), or if you really need to sync between state and props, remember not to unconditionally update.
Mutating props directly is problematic, must update derived states carefully, especially when your states are objects.