Hi, I'm Tuan, a Full-stack Web Developer from Tokyo 😊. Follow my blog to not miss out on useful and interesting articles in the future.
Introduction
React is a popular front-end library that is easy to learn and use. However, there are some concepts that developers should understand in order to write efficient and performant code. In this article, we will discuss some gotchas and concepts related to how state and effects work in React.
Deriving the State
Sometimes, we need to create a state variable that depends on another state variable. One way to do this is to use the useEffect
hook and update the state variable that depends on the other one.
For example, let's say we have a select
element that displays a list of user IDs. We want to track the selected user ID using a userId
state variable. Here is how we might do this:
import { useState } from "react"
const users = [
{ id: "1", name: "User One" },
{ id: "2", name: "User Two" },
{ id: "3", name: "User Three" },
]
function Users () {
const [userId, setUserId] = useState("1")
return(
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
);
}
Now, let's say we want to show the selected user on the screen. We can create another state variable called selectedUser
and update it whenever userId
changes using the useEffect
hook.
import { useState, useEffect } from "react"
function Users () {
const [userId, setUserId] = useState("")
const [selectedUser, setSelectedUser] = useState(undefined)
useEffect(() => {
setSelectedUser(users.find(u => u.id === userId))
}, [userId])
return(
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option>Select a user</option>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
{selectedUser && <p>The selected user is: {selectedUser.name}</p>}
</>
);
}
This approach works, but it is not the most efficient way to update selectedUser
. The problem with this method is that the component first renders when userId
changes, and then the useEffect
hook is triggered after the render because userId is passed in its dependency array. This means that the component renders twice just to update selectedUser
, first when userId
changes and second when the useEffect hook updates selectedUser
.
A better approach is to simply derive the value of selectedUser
directly from userId
.
function Users () {
const [userId, setUserId] = useState("")
const selectedUser = users.find(u => u.id === userId)
return(
<>
<select value={userId} onChange={e => setUserId(e.target.value)}>
<option>Select a user</option>
<option value="1">User One</option>
<option value="2">User Two</option>
<option value="3">User Three</option>
</select>
{selectedUser && <p>The selected user is: {selectedUser.name}</p>}
</>
);
}
This works because selectedUser
is derived directly from userId and is not tracked as a separate state variable. This means that the component will only re-render when userId changes, rather than re-rendering twice as in the previous example.
Set State inside Event Handlers
Sometimes, we need to update the state inside event handlers, such as a click event or a form submission event. In these cases, it's important to make sure that the state update is performed asynchronously, so that the component has a chance to re-render before the event handler finishes executing.
Here is an example of how to correctly set the state inside an event handler:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>The count is: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
In this example, we have a Counter
component that displays a count and has a button that increments the count when clicked. The handleClick
event handler updates the count
state variable using the setCount
function.
It's important to note that the setCount
function is asynchronous, which means that the state update may not be immediately reflected in the component. Instead, the component will re-render after the event handler finishes executing.
If we need to perform an operation that depends on the current value of the state, we can pass a function to the setCount
function, like this:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((currentCount) => currentCount + 1);
};
return (
<div>
<p>The count is: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
In this example, the setCount
function is called with a function that takes the current value of count
as an argument and returns the new value. This ensures that the state update is performed correctly, even if the component has not yet had a chance to re-render.
Cleanup Functions
Sometimes, we need to perform some cleanup actions when a component is unmounted or when a certain condition is no longer met. In these cases, we can use a cleanup function with the useEffect
hook.
A cleanup function is a function that is called when a component is unmounted or when the dependencies of an useEffect
hook change. It is a good place to perform any necessary cleanup, such as canceling network requests or removing event listeners.
Here is an example of how to use a cleanup function with the useEffect
hook:
function Users() {
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`/users/${userId}`);
const data = await response.json();
setUser(data);
};
fetchUser();
return () => {
// Cleanup function
console.log("Cleaning up...");
};
}, [userId]);
return (
<div>
{user ? (
<p>The user is: {user.name}</p>
) : (
<p>Loading...</p>
)}
<button onClick={() => setUserId(2)}>Load User 2</button>
</div>
);
}
In this example, we have a Users
component that fetches a user from a backend API based on the userId
state variable. The useEffect
hook is used to trigger the fetch when userId
changes.
The useEffect
hook also has a cleanup function that is called when the component is unmounted or when the userId
state variable changes. In this case, the cleanup function simply logs a message to the console.
It's important to note that the cleanup function is only called when the dependencies of the useEffect
hook change or when the component is unmounted. This means that if the userId
state variable changes but the useEffect
hook's dependencies do not, the cleanup function will not be called.
Updating the State when a Prop changes
Sometimes, we need to update the state of a component based on a prop that is passed to the component. In these cases, we can use the useEffect
hook to detect when the prop changes and update the state accordingly.
Here is an example of how to update the state when a prop changes:
function User({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch(`/users/${userId}`);
const data = await response.json();
setUser(data);
};
fetchUser();
}, [userId]);
return user ? <p>The user is: {user.name}</p> : <p>Loading...</p>;
}
In this example, we have a User
component that fetches a user from a backend API based on the userId
prop. The useEffect
hook is used to trigger the fetch when the userId
prop changes.
It's important to note that the useEffect
hook's dependencies array should include any props or state variables that the effect depends on. In this case, the effect depends on the userId
prop, so it is included in the dependencies array.
If the useEffect
hook's dependencies are not specified correctly, the effect may be triggered unnecessarily, which can lead to performance issues.
State preservation in the same position
Sometimes, we need to preserve the state of a component when it is re-rendered in the same position on the page. In these cases, we can use the useState
hook with the useMemo
hook to preserve the state.
The useMemo
hook is a performance optimization that allows us to memoize values so that they are only recomputed when the dependencies of the hook change. This can be useful for preserving the state of a component when it is re-rendered in the same position.
Here is an example of how to use the useMemo
hook to preserve the state of a component:
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
const increment = useMemo(() => {
return () => setCount(count + 1);
}, [count]);
return (
<div>
<p>The count is: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
In this example, we have a Counter
component that displays a count and has a button that increments the count
when clicked. The increment
function is created using the useMemo
hook and is only recomputed when the count
state variable changes. This means that the increment
function will always have the correct value of count
, even if the Counter
component is re-rendered in the same position on the page.
It's important to note that the useMemo
hook's dependencies array should include any props or state variables that the memoized value depends on. In this case, the increment
function depends on the count
state variable, so it is included in the dependencies array.
If the useMemo
hook's dependencies are not specified correctly, the memoized value may be recomputed unnecessarily, which can lead to performance issues.
Conclusion
We discussed several concepts related to state and effects in React. We covered topics such as deriving the state, setting the state inside event handlers, using cleanup functions with the useEffect hook, updating the state when a prop changes, and preserving the state of a component when it is re-rendered in the same position on the page.
Understanding these concepts is important for writing efficient and performant code in React, as well as for understanding how state and effects work in the React framework.
As always, I hope you enjoyed this article and learned something new. Thank you and see you in the next articles!
If you liked this article, please give me a like and subscribe to support me. Thank you. 😊