Our Blog
We help junior tech professionals, such as developers and designers, to grow.
Best Practices for useState and useEffect in React
Mariana Caldas 2024-01-18
This article cover image was created with the assistance of DALL·E 3
Welcome to the world of React! A place where creating dynamic web pages is fun, exciting, and quite confusing sometimes. You've probably heard of useState
and useEffect
hooks. These are like tools in a toolbox, each with a special purpose. Let's make sense of when and how to use them, especially focusing on avoiding common mix-ups.
Meet useState
: your single component state manager
Think of useState
as a notebook where you jot down important things that your web page needs to remember. It's perfect for keeping track of things like user input, or calculations based on that input.
When to use useState
-
Remembering values: Use
useState
for things your component needs to remember and change, like a user's name or a to-do list item. -
Calculating on the fly: When you have a value that needs to be updated based on other things you’re remembering (like adding up prices in a shopping cart),
useState
is perfect. That’s called a derived state.
Initializing state from props
Another powerful aspect of useState
is initializing state based on props
. This approach is particularly useful when your component needs to start with a predefined value, which can then be updated based on user interactions or other changes within your component.
In the example below, the WelcomeMessage
component receives initialMessage
as a prop and uses it to set the initial state for message
. This state can later be updated, for example, in response to user actions.
function WelcomeMessage({ initialMessage }) {
const [message, setMessage] = useState(initialMessage);
return <h1>{message}</h1>;
}
Introducing useEffect
: the side-effect handler
Now, let's talk about useEffect
. It's like a helper who does tasks outside of what's on the screen, like fetching data from the internet, setting up subscriptions, or doing things after your component shows up on the page.
When to use useEffect
-
Fetching data: If you need to grab data from somewhere else (like a server),
useEffect
is your go-to. -
Listening to events: Setting up things like timers or subscriptions is what
useEffect
excels at. -
Cleaning up:
useEffect
can also clean up after itself, which is handy for things like removing event listeners.
Let's explore scenarios that demonstrate the strength of useEffect
.
Example: fetching user data
Fetching data from an API is a classic use case for useEffect
.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Asynchronous function to fetch user data
async function fetchUserData() {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
setUser(userData);
}
fetchUserData();
}, [userId]);
// JSX for displaying user information...
}
In this case, useEffect
is ideal because data fetching is asynchronous and doesn't directly involve rendering the UI. The effect runs after rendering, ensuring that fetching data doesn't block the initial rendering of the component.
Example: window resize listener
Listening to the browser's window resizing and adjusting the component is a great use of useEffect
. This is a side effect because it involves interacting with the browser's API and needs to be set up and cleaned up properly, which useEffect
handles elegantly.
function ResponsiveComponent() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWindowWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
// Cleanup to remove the event listener
return () => window.removeEventListener('resize', handleResize);
}, []);
// JSX that adapts to the window width...
}
A common mistake: misusing useEffect
for derived state
A common mix-up is using useEffect
for tasks that useState
can handle more efficiently, like deriving data directly from other state or props. Let's clarify this with an example.
Bad example: misusing useEffect
for filtering a list
Imagine you have a list of fruits and you want to show only the ones that match what the user types in a search box. You might think about using useEffect
like this:
import React, { useState, useEffect } from 'react';
const fruits = ["Apple", "Banana", "Cherry"];
function FruitList() {
const [searchTerm, setSearchTerm] = useState('');
const [filteredFruits, setFilteredFruits] = useState(fruits);
useEffect(() => {
setFilteredFruits(fruits.filter(fruit =>
fruit.toLowerCase().includes(searchTerm.toLowerCase())
));
}, [searchTerm]);
return (
<div>
<h1>Search Filter</h1>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{filteredFruits.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
In this example, we're using useEffect
to update filteredFruits
every time searchTerm
changes. This approach, however, isn't ideal. Why?
-
Unnecessary Complexity:
useEffect
introduces an additional layer of complexity. It requires tracking and updating another state (filteredItems
) which depends onsearchTerm
. -
Risk of Errors: With
useEffect
, you run the risk of creating bugs or performance issues, especially if your effect interacts with other states or props in complex ways. -
Delayed Update: Since effects run after render, there can be a slight delay in updating
filteredItems
, potentially leading to a brief mismatch in what the user sees.
Better way: using useState for derived state
A simpler and more effective approach is to calculate the filtered list directly using useState
, like this:
import React, { useState } from 'react';
const fruits = ["Apple", "Banana", "Cherry"];
function FruitList() {
const [searchTerm, setSearchTerm] = useState('');
const filteredFruits = fruits.filter(fruit =>
fruit.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div>
<h1>Search Filter</h1>
<input
type="text"
placeholder="Search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{filteredFruits.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
In this approach, filteredItems
is recalculated immediately whenever searchTerm
changes. This ensures the filtered list is always in sync with the user's input, without the extra step and delay of useEffect
. It's simpler, more efficient, and reduces the chance of errors related to state synchronization.
Other common pitfalls and best practices
Forgetting dependencies in useEffect: A typical mistake is not correctly specifying the dependency array, leading to unexpected behavior.
An example of that would be omitting [userId]
in the dependency array of the UserProfile
useEffect
example above, which could prevent the component from updating when the user changes.
Incorrect state updates: In useState
, ensure to use the functional update form when the new state depends on the previous one.
Example:
function Counter() {
const [count, setCount] = useState(0);
// Correct usage
const increment = () => {
setCount(prevCount => prevCount + 1);
};
//Incorrect usage
// const increment = () => {
// setCount(count + 1);
// };
// JSX for displaying and updating the count...
}
Using the functional update form in useState
, like setCount(prevCount => prevCount + 1)
, ensures that you always have the most recent state value, especially important in scenarios where the state might change rapidly or in quick succession, such as in fast user interactions.
Conclusion
In React, useState
and useEffect
serve distinct purposes. useState
is your go-to for tracking and reacting to changes within your component, ideal for direct state management and calculations based on state. On the other hand, useEffect
is perfect for handling external operations and side effects, like fetching data or interacting with browser APIs. Understanding when to use each will greatly improve the efficiency and reliability of your React components.