A few weeks ago I wrote an article all about mistakes that developers sometimes make when working with React component state. All of the examples I provided were using class components and the setState
method.
3 React Mistakes Junior Developers Make With Component State
Tyler Hawkins ・ Jun 15 '20
I was asked several times if these same principles applied to function components and hooks. The answer is yes!
By popular demand, in this article we'll explore those same concepts, but this time with function components using the useState
hook. We'll look at three common mistakes and how to fix them.
1. Modifying state directly
When changing a component's state, it's important that you return a new copy of the state with modifications, not modify the current state directly. If you incorrectly modify a component's state, React's diffing algorithm won't catch the change and your component won't update properly.
Let's look at an example. Say that you have some state that looks like this:
const initialState = ['red', 'blue', 'green']
let [colors] = useState(initialState)
And now you want to add the color "yellow" to this array. It may be tempting to do this:
colors.push('yellow')
Or even this:
colors = [...colors, 'yellow']
But both of those approaches are incorrect! When updating state in a function component, you always need to use the setter method provided by the useState
hook, and you should always be careful not to mutate objects. The setter method is the second element in the array that useState
returns, so you can destructure it just like you do for the state value.
Here's the right way to add the element to the array:
// Initial setup
const initialState = ['red', 'blue', 'green']
const [colors, setColors] = useState(initialState)
// Later, modifying the state
setColors(colors => [...colors, 'yellow'])
And this leads us right into mistake number two.
2. Setting state that relies on the previous state without using a function
There are two ways to use the setter method returned by the useState
hook. The first way is to provide a new value as an argument. The second way is to provide a function as an argument. So, when would you want to use one over the other?
If you were to have, for example, a button that can be enabled or disabled, you might have a piece of state called isDisabled
that holds a boolean value. If you wanted to toggle the button from enabled to disabled, it might be tempting to write something like this, using a value as the argument:
// Initial setup
const [isDisabled, setIsDisabled] = useState(false)
// Later, modifying the state
setIsDisabled(!isDisabled)
So, what's wrong with this? The problem lies in the fact that React state updates can be batched, meaning that multiple state updates can occur in a single update cycle. If your updates were to be batched and you had multiple updates to the enabled/disabled state, the end result may not be what you expect.
A better way to update the state here would be to provide a function of the previous state as the argument:
// Initial setup
const [isDisabled, setIsDisabled] = useState(false)
// Later, modifying the state
setIsDisabled(isDisabled => !isDisabled)
Now, even if your state updates are batched and multiple updates to the enabled/disabled state are made together, each update will rely on the correct previous state so that you always end up with the result you expect.
The same is true for something like incrementing a counter.
Don't do this:
// Initial setup
const [counterValue, setCounterValue] = useState(0)
// Later, modifying the state
setCounterValue(counterValue + 1)
Do this:
// Initial setup
const [counterValue, setCounterValue] = useState(0)
// Later, modifying the state
setCounterValue(counterValue => counterValue + 1)
The key here is that if your new state relies on the value of the old state, you should always use a function as the argument. If you are setting a value that does not rely on the value of the old state, then you can use a value as the argument.
3. Forgetting that the setter method from useState
is asynchronous
Finally, it's important to remember that the setter method returned by the useState
hook is an asynchronous method. As an example, let's imagine that we have a component with a state that looks like this:
const [name, setName] = useState('John')
And then we have a method that updates the state and then logs the state to the console:
const setNameToMatt = () => {
setName('Matt')
console.log(`The name is now... ${name}!`)
}
You may think that this would log 'Matt'
to the console, but it doesn't! It logs 'John'
!
The reason for this is that, again, the setter method returned by the useState
hook is asynchronous. That means it's going to kick off the state update when it gets to the line that calls setName
, but the code below it will continue to execute since asynchronous code is non-blocking.
If you have code that you need to run after the state is updated, React provides the useEffect
hook, which allows you to write code that gets run after any of the dependencies specified are updated.
(This is a bit different from the way you'd do it with a callback function provided to the setState
method in a class component. For whatever reason, the useState
hook does not support that same API, so callback functions don't work here.)
A correct way to log the current state after the update would be:
useEffect(() => {
if (name !== 'John') {
console.log(`The name is now... ${name}!`)
}
}, [name])
const setNameToMatt = () => setName('Matt')
Much better! Now it correctly logs 'Matt'
as expected.
(Note that in this case I've added the if
statement here to prevent the console log from happening when the component first mounts. If you want a more general solution, the recommendation is to use the useRef hook to hold a value that updates after the component mounts, and this will successfully prevent your useEffect
hooks from running when the component first mounts.)
Conclusion
There you have it! Three common mistakes and how to fix them. Remember, it's OK to make mistakes. You're learning. I'm learning. We're all learning. Let's continue to learn and get better together.
If you'd like to check out some live demos for the examples used here (and more), visit http://tylerhawkins.info/react-component-state-demo/build/.
You can also find the code on GitHub.
Top comments (28)
I don't like the title at all.
There are no mistakes that are exclusive or specific to junior developers.
In fact there are so many senior developers that do not write better code than many junior developers.
Very true! These are mistakes that anyone unfamiliar with React or with hooks may make.
I understand your point and I share with you that generalization is bad and there is no a simple rule to say "This is Junior, this is Senior".
In this case, these 3 issues will be present to every person trying to learn React. Probably because they're not intuitive and people without knowledge of something will try to use logic to fill the gaps.
So I think there is some grey area where even if the title is correct, it could say something friendlier like "3 mistakes every person learning react (...)".
By the way, nice post!
Totally. Phrasing can be hard. I'm going to leave the title as is on this one, but this is some great feedback to be aware of in the future, and I appreciate that.
Maybe I am not as strong in this conviction as not liking the title at all, for Junior developers are at as much of a need for humility as everyone else writing code (which is why everyone should use Linux, it tears you down every time your head gets too big and in the middle of that huge problem you eat humble pie while clicking through an installer) but Dan is onto something in pointing out that there is something a little off about singling out Junior Developers with the title.
We all know, including you, that in reality these are mistakes that people just make in general, mostly because its never very well explained anywhere what the specific rules are, except in this well written article of course, but I think its more fair to exclude the experience based qualifier of Junior when a 20 year veteran of low level C programming is still liable to make the mistakes because she happens to not have noticed that in any documentation she read. The only thing that is true of Junior Developers specifically is the inclination to quit, which others might like for job security, but I think is a good reason to not target them for fear of discouraging them in a field where the rising tide lifts all ships in the harbor.
Thank you! I am just learning React. Your article helped me to understand why it is done in a certain way. It's good to know the common mistakes right away, so I don't "practice" the wrong way for very long. I'll be on the lookout for these.
You are welcome! Happy learning. :)
thanks a lot
your article just came to my rescue because I was working on a react project and I was doing all of these mistakes especially the first point I was modifying an array(hook) directly and wondering why the component wasn't updated accordingly.
You’re welcome! Glad I could help. :)
Great article! Simple, yet effective. Thank you
Thanks for reading!
3very good points.
On (1), you should explain why the setter and immutable data are needed in React.FC.
Thanks, good suggestion! How's something like this:
As far as I understand it, the simplest way to explain why data should be immutable in React is that React components are functions of props and state, so when the diffing algorithm is deciding whether or not to update the UI for any given component, it compares the previous props and state with the current props and state.
So if you mutate the previous state, the comparison between the previous state and the current state will actually show that it's the same object reference with the same data, so it will incorrectly see that there's nothing new to show.
Exactly.
To paraphrase your reply.
1) Immutability means data modifications will result in new instances; so explicit comparisons are supported and deep-comparisons can be avoided.
2) The setter function also implicitly triggers the FC to asynchronously re-render. Without the setter, the data would be updated, but the FC would not redraw to reflect the current data state.
MOOAR. Thanks for taking the time to write this.
You're welcome!
I've been learning react for a month and I never realized I was making those mistakes, thank you a lot.
You’re welcome! Keep on learning and being awesome.
Good article, you pick up good points, thanks.
Thank you Gabriel!
I wasn't aware I was making these mistakes. Thanks for correcting Tyler.
Sure thing! Learning more and getting better is what it’s all about.
Thank you for this refresher!
You're welcome!