Mastering useState and useEffect in React: The Heart of Functional Components
React is all about building user interfaces using small, reusable pieces called components. In modern React, most components are written as functions rather than classes. But for these function components to be truly powerful, they need to manage data and respond to changes over time. This is where two essential hooks come in: useState and useEffect.
If you are comfortable with basic React syntax but still find yourself confused about when and how to use these hooks, this article is for you.
Understanding useState: Managing Data Inside Components
Every interactive part of a React app needs to remember something, for example a button click count, a form input, or whether a menu is open or closed. The useState hook lets your component store and update this kind of information.
You can think of it as giving your component a small piece of memory.
Example: A Simple Counter
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You have clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}Let’s break it down:
- useState(0) means you start with an initial value of zero.
- React returns an array with two parts:
count, the current valuesetCount, a function to change it
3. Every time you call setCount, React updates the value and re-renders the component with the new state.
You can store any kind of data with useState, for example numbers, strings, arrays, or even objects. The most important thing to remember is that when the state changes, the user interface updates automatically.
Understanding useEffect: Handling Side Effects
While useState manages data inside your component, useEffect allows your component to perform actions that affect things outside React’s rendering process. These actions include fetching data, listening to browser events, or updating the page title.
These actions are called side effects because they happen as a result of something else that occurs in the component.
Example: Changing the Document Title
import React, { useState, useEffect } from 'react';
function TitleChanger() {
const [name, setName] = useState('Guest');
useEffect(() => {
document.title = `Welcome, ${name}`;
}, [name]);
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
);
}Here’s what is happening:
- When the component first loads, it sets the document title to “Welcome, Guest.”
- When you type something new, the name state changes.
- Because name is listed in the dependency array
[name], the effect runs again and updates the title.
The dependency array tells React when to re-run the effect.
If the array is empty, the effect runs only once when the component is first displayed.
If you include variables, React runs the effect again whenever those variables change.
A Common Example: Fetching Data
One of the most popular uses of useEffect is fetching data from an API when the component loads.
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Here’s the step-by-step process:
- The component mounts for the first time.
- The effect runs once because of the empty dependency array.
- It fetches user data and updates the users state.
- React re-renders the component to display the list of names.
The Relationship Between useState and useEffect
You can think of useState and useEffect as two parts of a single system.
useState holds your component’s data.
useEffect reacts to changes in that data and performs actions outside React.
Get Long Nguyen’s stories in your inbox
Join Medium for free to get updates from this writer.
Together, they make your components dynamic and responsive.
Example: A Countdown Timer
import React, { useState, useEffect } from 'react';
function Countdown() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
if (seconds > 0) {
const timer = setTimeout(() => setSeconds(seconds - 1), 1000);
return () => clearTimeout(timer);
}
}, [seconds]);
return <h2>Time left: {seconds}</h2>;
}In this example:
- The state starts at ten seconds.
- The effect runs every second, decreasing the counter.
- The cleanup function clears the timeout to prevent duplicate timers when the component updates.
This shows how useEffect can both start and clean up work automatically.
Cleanup with useEffect
Not every effect needs cleanup, but when you add event listeners, subscriptions, or timers, you should clean them up when the component unmounts or before running the effect again.
React handles this by running the function returned inside useEffect.
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);In this example, React removes the event listener when the component is no longer needed, preventing memory leaks and unwanted behavior.
Common Mistakes and How to Avoid Them
- Forgetting the dependency array
Without it, the effect runs after every render, which can cause performance issues or even infinite loops. - Updating state inside an effect without conditions
If you change state unconditionally inside an effect, it can trigger another render, leading to an endless cycle. - Not cleaning up side effects
Always remove event listeners, subscriptions, or timers when the component unmounts.
When to Use Each Hook
It can sometimes be confusing to decide whether a situation requires useState or useEffect.
Here is a simple way to think about when to use each one.
- Store and update local data
Use useState when your component needs to remember or modify information, such as form input values, toggle states, or counters.
This data exists only inside the component and changes based on user interaction or internal logic. - Run code when the component mounts or updates
Use useEffect when something needs to happen after the component appears on the screen or when certain values change.
For example, you might want to log information, start a timer, or update an element outside React. - Fetch data from an API
Use useEffect to load external data, such as fetching a list of users or products from a server when the component loads.
The effect should usually include an empty dependency array so it runs only once when the component is first displayed. - Respond to user input
Use useState when you need to capture and update data as the user interacts with the component.
For example, when typing in a search box or clicking a button that changes what is displayed. - Synchronize with external systems such as the document title or local storage
Use useEffect when you need to keep something outside React in sync with your component’s state.
Common examples include updating the page title, saving user preferences to local storage, or connecting to a browser API.
Final Thoughts
The useState and useEffect hooks are the core of modern React development.
Once you understand how they work together, you can build interactive and data-driven components that respond gracefully to user input and external changes.
Start with small examples and experiment until these two hooks feel natural.
When you do, you will find that React’s functional components become much more powerful and enjoyable to use.






