Engineering

Chantelle Chan, Angela Wang

·

Mar 8, 2023

Elegantly managing state and renders in React

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.

TLDR:

  • React states behave more like a snapshot in time than a regular Javascript variable.

  • 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 useState:

Unfortunately, count never increments. This is because

  1. Local variables don’t persist between renders. (Practically: count is re-initialized to 0 on each render.)

  2. 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:

  1. Retain the data between renders.

  2. 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?

Still 0!

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

Since 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:

  1. React queues this function to be processed after all the other code in the event handler has run.

  2. 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.

React treats setState(newValue) as setState(currentValue ⇒ newValue), and also queues up the “function”, except that the queued value (currentValue) is ignored and replaced with what was specified (newValue).

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:

For more on what we're learning as we build Remotion, subscribe to our tech blog.

Engineering

Chantelle Chan, Angela Wang

Mar 8, 2023

·

Elegantly managing state and renders in React

© Multi Software Co. 2023

© Multi Software Co. 2023