Ever tried to access a state variable in React just after updating it and wondered why you still get the previous value? Yes? Well, by the end of this article, you should understand why this behavior occurs in React and how to work around it.
Typically, when you want to update a state variable, you would utilize the setter function made available at initialization.
const [name, setName] = useState('');
In the expression above, setName
is the setter function that lets us update the name variable, triggering a re-render and update of the UI. However, some times we would like to immediately access the state's newly updated value in order to perform some other action.
Let's jump right into a real-world example.
Let's say we have a component that simply collects a book title query, displays it and also provides a list of suggestions using the title as a query.
We should be able to see live updates as we change the value in the input box. Every time, we edit the value, the setter function, setTitle
, runs and React triggers a re-render of the component and the UI updates to the new title
value.
What if we want to immediately access this title to send to an API endpoint as a query in order to access a list of books that our user may be searching for i.e. suggestions? Here is how that may look like:
// searchBooks.jsx
export default function searchBooks() {
const [title, setTitle] = useState("");
function handleInputChange(e) {
setTitle(e.target.value);
try {
const url = `https://www.mybooks.com/books/suggestions?q=${title}`;
fetch(url)
.then((response) => response.json())
.then((data) => console.log(data));
} catch (error) {
console.log(error);
// send to logging service
}
}
return (
// ...
);
}
If all goes well, we expect our GET request to return an array of 3 book suggestions that are similar to our query, which we can then iterate over to display to our users. When they click on it, they would then be redirected to the appropriate page.
For now, we would replace the GET request to our non-existent API with a simple promise that simulates a response from the backend and resolves with a list of suggestions from our database after a second(1000ms).
// data.js
// simulates database
export const data = [
{
title: "The Birth of Tragedy",
author: "Friedrich Nietzsche",
url: "https://www.mybooks.com/books/the-birth-of-tragedy",
},
{
title: "Genghis: Birth of an Empire",
author: "Conn Iggulden",
url: "https://www.mybooks.com/books/genghis-birth-of-an-empire",
},
{
title: "Transcendent Kingdom",
author: "Yaa Gyasi",
url: "https://www.mybooks.com/books/transcendent-kingdom",
},
{
title: "Beyond Good and Evil ",
author: "Friedrich Nietzsche",
url: "https://www.mybooks.com/books/beyond-good-and-evil",
},
{
title: "Billy Summers",
author: "Stephen King",
url: "https://www.mybooks.com/books/billy-summers",
},
];
import "./styles.css";
import { useState, Fragment } from "react";
import { data } from "./data";
export default function searchBooks() {
const [title, setTitle] = useState("");
const [suggestions, setSuggestions] = useState([]);
const getSuggestions = () => {
try {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const filteredSuggestions = title
? data
?.filter((suggestion) =>
suggestion.title.toLowerCase().includes(title.toLowerCase())
)
?.slice(0, 3)
: [];
resolve(filteredSuggestions);
}, 1000);
}).then((res) => setSuggestions(res));
promise;
} catch (error) {
console.log(error);
// send to logging service
}
};
function handleInputChange(e) {
setTitle(e.target.value);
getSuggestions();
}
return (
<div className="App">
<input type="text" value={title} onChange={(e) => handleInputChange(e)} />
<h3>Book title: {title}</h3>
{suggestions?.map((suggestion) => (
<Fragment key={suggestion.title}>
<a href={suggestion.url}>{suggestion.title}</a>
<br />
</Fragment>
))}
</div>
);
}
If you don't know how useState works yet, you can learn more about it here.
The expected behavior of our component after a user enters a search query is to:
set the
title
state to the newly entered query in ourhandleInputChange
event handlerfilter the data/books array using the newly set
title
return the filtered data as a list of suggestions
store this array in a
'suggestions'
state variable that triggers anupdate of the UI with a list of suggestions
When we run this code however, we see that there's no live update of the UI with suggested books as we change the search query. We know we are updating the title because the <h3>
tag is being updated. However, we see only old updates where it seems like our 'database' is being filtered by old title
data.
To debug, let's try logging our title
variable before the promise.
function getSuggestions(e) {
try {
console.log(title)
const promise = new Promise((resolve, reject) => {
// ...
});
promise;
} catch (error) {
console.log(error);
// send to loggin service
}
}
From the video above, we can see that we keep logging the previous title
value instead of the new one. Therefore, it is the previous value that is being used to filter the array.
What causes this behavior?
When we update state using the setter function in a render, Render #1, we do not change the state directly. Instead, we trigger a re-render, Render #2, which would then have the newly updated value. We do not have access to the new value in the current render. Setting state only changes it for the next render.
For our example above, when we change the value in our input box, the following happens:
the
handleInputChange()
event handler executessetTitle
setstitle
to the value in our textbox and requests a re-render from ReactReact re-renders the component with the new
title
value (Render #2)
Therefore, we can see that Render #1, our render where we set the title
value, would not have access to the new value of title
. However, Render #2, would.
This explains the "delayed" behavior we see when we log our title
value. The render (Render #1) that logs the title
value, still only has access to the current value and not the updated/new title
value.
React takes snapshots of your render within the limits of time. This snapshot is the JSX returned from calling your component. When it calls your component, it calculates your props, functions, event handlers and variables using the state at the time of the render. React then updates the screen based on this snapshot of your component. This snapshot is interactive, in that you can interact with it and expect a response based on the logic already present in that snapshot e.g. event handlers.
This is how React re-renders your component. When you set state, React:
calls your function again
which returns a new JSX snapshot that has been computed based on the state change. In this step, the new value is provisioned and used to calculate the event handlers and other functions that use it.
React then updates the screen to match the snapshot your function (component) returned. In this step, the
<h3>
tag now shows the new title.
React keeps the state values “fixed” within one render’s event handlers. Variables and event handlers don’t “survive” re-renders. Every render has its own event handlers and React waits until all code in the event handlers has run before processing your state updates.
This is why logging the title returns the value before the update. It's because that line of code ran using a snapshot of the state at the time the user interacted with it. The state simply doesn't change while the code is still running!
Let's say we change our title from 'tra' to 'trag', here's a representation of what happens:
In Render #1, React computes our snapshot using 'tra' as the
title
state.Then, we change
title
from 'tra' to 'trag' in the input box and our event handler runsThe event handler sets state to 'trag' and React queues a new render with 'trag' as the state
The block of code in the event handler is not done yet. It runs
getSuggestions(
). Remember we are still in Render #1 which still believestitle
state is 'tra'.In
getSuggestions()
where we use thetitle
value, we log 'tra' to the console and filter our books data array with 'tra' not 'trag'.React creates a new JSX snapshot with 'trag' as the
title
, calculating the event handler andgetSuggestions()
with that data.It also updates the screen with this new value as the
title
state so we can see the<h3>
tag change to 'trag' (Render #2)
How do we handle this?
One way to 'fix' this, i.e. access the new title value in the current render, is to use a useEffect hook to subscribe to changes in the title
state and also perform side effects. When there's a change to title
, thegetSuggestions()
function is run with the new value in the current render.
import "./styles.css";
import { useState, Fragment, useEffect } from "react";
import { data } from "./data";
export default function searchBooks() {
const [title, setTitle] = useState("");
const [suggestions, setSuggestions] = useState([]);
const getSuggestions = () => {
try {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const filteredSuggestions = title
? data
?.filter((suggestion) =>
suggestion.title.toLowerCase().includes(title.toLowerCase())
)
?.slice(0, 3)
: [];
resolve(filteredSuggestions);
}, 1000);
}).then((res) => setSuggestions(res));
promise;
} catch (error) {
console.log(error);
// send to logging service
}
};
useEffect(() => {
getSuggestions();
}, [title]);
function handleInputChange(e) {
setTitle(e.target.value);
}
return (
// ...
);
}
As you can see above, getSuggestions()
logs the new value of title
as it is being updated via the input element and event handler.
Another simple way would also be to pass the data to be updated directly to the getSuggestions()
function.
const getSuggestions = (query) => {
try {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const filteredSuggestions = query
? data
?.filter((suggestion) =>
suggestion.title.toLowerCase().includes(query.toLowerCase())
)
?.slice(0, 3)
: [];
resolve(filteredSuggestions);
}, 1000);
}).then((res) => setSuggestions(res));
promise;
} catch (error) {
console.log(error);
// send to logging service
}
};
function handleInputChange(e) {
setTitle(e.target.value);
getSuggestions(e.target.value);
}
A way to improve performance would be to debounce the onChange
event handler in order to limit the frequency of the asynchronous requests to the API or, in our case, function calls.
Side Quest🚀
If you name a setter function without the prefix, set-, what happens?🧐
const [title, makeTitle] = useState("");
function handleInputChange(e) {
makeTitle(e.target.value);
}
Nothing at all. It's just a naming convention😉
Recap
I hope you learned something and now understand the snapshot behavior of React in setting state variables and component rendering, and also how to address it.
Please leave a like if this was helpful. Comment also if you have questions.
Happy Coding🙌