When you start learning React, two hooks dominate almost every component you write: useState and useEffect. They look simple at first glance, but the distinction between them is fundamental — and misunderstanding when to use each is one of the most common sources of bugs and over-complicated code.
What is useState?
useState is React's mechanism for storing and updating values that belong to a component. When state changes, the component re-renders, and the UI reflects the new value. It's synchronous in terms of triggering a re-render, and its sole purpose is to track data that the component owns.
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
In this example, count is the current state value, setCount is the function to update it, and 0 is the initial value. Every time you click the button, setCount triggers a re-render with the updated count.
Key characteristics of useState
- Stores a single value (which can be any type — number, string, object, array)
- Re-renders the component when the value changes
- State is local to the component (unless passed via props or shared via context)
- The setter function can accept a new value or an updater function
- State updates may be batched for performance
What is useEffect?
useEffect is for side effects — operations that need to happen outside of the render cycle. This includes fetching data, subscribing to events, setting up timers, directly manipulating the DOM, and integrating with third-party libraries.
import { useState, useEffect } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
}, [userId])
if (!user) return <p>Loading...</p>
return <p>Hello, {user.name}</p>
}
The second argument to useEffect — the dependency array — controls when the effect runs. If userId changes, the effect re-runs and fetches new data. This is the critical pattern for synchronising your component with external data sources.
The dependency array
- No array — effect runs after every render
- Empty array
[]— effect runs once, on mount only - Array with values — effect runs when any listed dependency changes
The Core Difference
useState answers: "What data does this component need to track?"
useEffect answers: "What should happen in response to the component mounting, updating, or unmounting?"
Put another way: useState is about what your component knows. useEffect is about what your component does as a consequence of rendering.
Quick Comparison
| Feature | useState | useEffect |
|---|---|---|
| Purpose | Store & update component state | Handle side effects |
| When it runs | When setter is called | After render, based on deps |
| Triggers re-render | Yes | Not directly |
| Use for | UI data, form inputs, toggles | Data fetching, subscriptions, timers |
| Cleanup | Not applicable | Return a cleanup function |
Common Patterns
Fetching data on mount
useEffect(() => {
fetchPosts().then(data => setPosts(data))
}, []) // empty array = runs once on mount
Responding to a prop change
useEffect(() => {
setFilteredItems(items.filter(i => i.tag === selectedTag))
}, [items, selectedTag])
Cleanup — unsubscribing
useEffect(() => {
const subscription = someEvent.subscribe(handler)
return () => subscription.unsubscribe() // cleanup on unmount
}, [])
A Practical Rule of Thumb
If you're tracking a value that your component needs to display or compute from — use useState. If you're doing something that interacts with the outside world (the network, the DOM, a timer, a subscription) — use useEffect. Often you'll use both together: fetch data in an effect, store it in state, and display it in the render.
One thing to avoid: don't use useEffect to transform state that could be derived during render. If you can compute a value directly from existing state or props without side effects, do it during the render — no hook needed.
Tags