Here's a button that renders a count in React. What do you expect the count to be after clicking it once?
If you answered "3", read on. If you answered "1", please visit our Careers page. (Just kidding, we're not hiring!)
In our efforts to improve streamline our user onboarding flow, we discovered that we’d been under-leveraging graceful state management provided by React. Users could have buggy experiences when diverting from the “expected happy path” due to event handling. Read on to learn how we simplify state management in React and get UI state updating when it’s supposed to.
Setting state only changes it for the next render.
React batches state updates and only renders after all code in event handlers are called.
React function components without useState: a snapshot in time
Have you ever googled:
state not updating in react?
Everyone and their mom’s beginners React tutorial involves the
useState hook, but few learn what's happening behind the scenes. Consider this code that doesn’t use
count never increments. This is because
Local variables don’t persist between renders. (Practically:
countis re-initialized to 0 on each render.)
Changes to local variables won’t trigger renders.
Rendering simply means that React is calling the component, which is a function. This can happen for a variety of reasons, from user navigation to reloads, but most commonly, it is because data changed higher up the component hierarchy.
What useState does
useState takes care of two important concerns:
Retain the data between renders.
Trigger React to change the component when the data changes (re-rendering).
This means the state ‘lives’ in React outside of the function component. When React calls the component, it gives the component and all its event handlers a snapshot of the state for that particular render. Consider this:
On button press,
0,0 is logged in the console. What happens if we wait just a little?
That’s because on initial render,
handleClick received a snapshot of the count, which was 0. The state variable
count will never change within the same render. Instead, the updated value of count will be passed to the next React render cycle, and React will be triggered to re-render the component. (Remember, that is one of
useState's two main responsibilities!)
A state variable’s value never changes within a render, even if its event handler’s code is asynchronous. - React Official Docs
setCount doesn’t update count during the same “run” of
handleClick(), we could save the value of count to a local variable, and increment this local variable to get the desired effect.
React will batch state change updates
Consider the following code, what should the
button of the
Counter component display after one click?
Counterintuitively, just 1! On initial render,
countToThree was given snapshot value of 0 for
count, and React waits until all code in the event handlers has run before processing state updates.
This means re-render only happens after all three
setCount calls, the last of which can be thought of as
setState(0 + 1). (The last call’s significance will become evident in just a moment). React does this to avoid triggering half-finished renders, and reduce the total number of re-renders.
To make the above code work, we could instead pass in an updater function:
React queues this function to be processed after all the other code in the event handler has run.
During the next render, React goes through the queue and provides the final updated state.
When a function is passed to a setState function instead of a value, the setState function will set the function parameter (in this case, c) to the current value of the state value.
But what happens if the state is replaced after an updater function?
In the next render, the value of count will end up as 0! Since the last
setCount() call passed a value instead of a function, the current value of
count was ignored, and set to the specified value instead.
setState(currentValue ⇒ newValue), and also queues up the “function”, except that the queued value (
currentValue) is ignored and replaced with what was specified (
So, if another
setCount(c => c + 1) was appended to the
countToThree() function above, the value of count on the next render would be 1.
Learn more about React at the official docs:
How does React know which state to return: Scroll to the Deep dive section on this page
State as a snapshot: https://beta.reactjs.org/learn/state-as-a-snapshot
Batch updating state: https://beta.reactjs.org/learn/queueing-a-series-of-state-updates
For more on what we're learning as we build Remotion, subscribe to our tech blog.