Jul 11th, 2022
The motivation for writing this blog was from a blocker that I encountered during a take home project. It is intended to illustrate a buggy attempt that was made along with a working solution. Even though this example is specific to implementing a certain task, the underlying concept of how state updates are made is fundamental to uunderstanding React under the hood.
The make believe example we will be covering is to build a form that takes an input of student names and return the submitted student list sorted in alphabetical order. Since this post will focus on how states are updated in React, the basics for how to set up a form will not be covered.
The student names are stored and updated within the useState
hook. values
is the variable that will store each new input and setValues
will update values
every time it is called. We initially set values
to an empty object.
const [values, setValues] = useState({});
The submitted student list is stored and updated within another useState
hook. submissions
is the variable that will store all submissions that are added when setSubmissions
is called. We initially set submissions
to mock data(for testing the behavior).
const [submissions, setSubmissions] = useState([{ name: 'John' }]);
The handleSortName
function is required to sort all of the names stored in submissions
. It is important to note that sort() will mutate the array which it is applied to, therefore a new array is created for the sorted submissions. localeCompare() will perform the sorting between each individual name and sort them in alphabetical order.
const handleSortName = (submissions) => {
return [...submissions].sort((a, b) => a.name.localeCompare(b.name));
};
Now back to the crux of this blog. When multiple updates are made to a component the latest changes may not be reflected on the DOM after the component renders. This is due to the fact that React state updates are asynchronous. This section of React's documentation and this entry on a very popular blog goes into more details.
With regards to our example the multiple operations performed are:
Initially, I attempted to apply a side-effect(sorting in our case) to submissions with the useEffect
hook. This setup will execute the handleSortName
callback whenever there is a change to submissions
.
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
As we can see the sorting behavior is one step behind and will not sort the most recent submission. Strangely the submitted student list will be sorted when a change is detected within the input value. This is not the expected behavior and there is a timing issue somewhere. 🤔
Instead of listening for changes and applying a side effect, we will execute handleSortName
before mapping through the submissions and rendering the <Submission />
component. This works because handleSortName
is executed before <Submission />
is rendered.
...
{ handleSortName(submissions).map((submission, index) => (
<Submission key={index} submission={submission} />
))}
...
Executing handleSortName
will then cause <Submission />
to re-render because there has been a change in its state. Finally the latest changes will be reflected on the DOM. The entire codebase with working solution can be found here.
In the context of using the latest values, another solution that can be applied are functional updates.
When a new student name is entered, the submitted student list will automatically be sorted as expected. 🙌
I hope by now you have a better understanding of how states are updated in React. The main takeaway is that they are batched together for performance and therefore asynchronous. Since React will re-render a component every time state(or props) changes, a common solution would be to update the state again in order for changes to be reflected on the DOM.