When Should I Use Derived State Instead of useEffect in React?
The Problem
I had a React component that needed to display filtered data based on a user’s selection. When the selected category changed, I wanted to update the filtered list. My first instinct was to use useEffect:
function ProductList({ products, selectedCategory }) { const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => { const filtered = products.filter(p => p.category === selectedCategory); setFilteredProducts(filtered); }, [products, selectedCategory]);
return ( <ul> {filteredProducts.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> );}This worked, but something felt wrong. I was managing extra state, triggering effects, and creating unnecessary re-renders. Then I discovered the pattern that changed everything: derived state.
The Anti-Pattern I Was Guilty Of
Looking back, I realized I had fallen into a common trap. A Reddit post on r/reactjs perfectly captured what I was doing:
“If prop A changed, I had an effect to update state B, which naturally triggered another effect to fetch some data, which updated state C”
This cascade of chained effects created code that was:
- Hard to debug
- Prone to stale state bugs
- Difficult to trace mentally
- Unnecessarily complex
The Reddit community echoed this experience:
“Same here, I used to throw useEffect at everything and it got messy fast. Once I started deriving stuff directly in render it felt way cleaner and fewer bugs too.”
The Realization That Changed Everything
The insight that shifted my approach came from a simple observation: you can derive variables during the render cycle.
Instead of:
- Props change
- useEffect triggers
- State updates
- Component re-renders
I could do:
- Props change
- Compute derived value directly
- Render with that value
The Solution: Derived State
Here’s the cleaner version of my ProductList component:
function ProductList({ products, selectedCategory }) { // Derive the value directly during render const filteredProducts = products.filter(p => p.category === selectedCategory);
return ( <ul> {filteredProducts.map(product => ( <li key={product.id}>{product.name}</li> ))} </ul> );}No useState. No useEffect. No dependency array. Just pure, derived data.
When to Use Derived State
Use derived state when:
-
You can compute a value from existing props or state
UserProfile.js function UserProfile({ user }) {// Derived: computed from propsconst displayName = user.nickname || user.email.split('@')[0];const isAdmin = user.role === 'admin';return <div>{displayName} {isAdmin && '(Admin)'}</div>;} -
You need to transform or filter data
TodoList.js function TodoList({ todos, filter }) {// Derived: filtering existing dataconst visibleTodos = todos.filter(todo => {if (filter === 'completed') return todo.completed;if (filter === 'active') return !todo.completed;return true;});return <TodoItems items={visibleTodos} />;} -
You need to combine multiple pieces of state
Cart.js function Cart({ items }) {// Derived: computed from itemsconst total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);return (<div><p>{itemCount} items - ${total.toFixed(2)}</p></div>);}
When You Actually Need useEffect
useEffect is for side effects, not state synchronization. Use it for:
-
API calls and data fetching
UserData.js function UserData({ userId }) {const [data, setData] = useState(null);useEffect(() => {fetch(`/api/users/${userId}`).then(res => res.json()).then(setData);}, [userId]);return data ? <Profile data={data} /> : <Loading />;} -
Subscriptions and event listeners
WindowSize.js function WindowSize() {const [size, setSize] = useState({ width: 0, height: 0 });useEffect(() => {const handleResize = () => {setSize({ width: window.innerWidth, height: window.innerHeight });};window.addEventListener('resize', handleResize);handleResize(); // Set initial sizereturn () => window.removeEventListener('resize', handleResize);}, []);return <span>{size.width} x {size.height}</span>;} -
DOM mutations
ScrollToTop.js function ScrollToTop({ children }) {useEffect(() => {window.scrollTo(0, 0);}, []);return children;}
The Pattern to Avoid
This is the anti-pattern that should raise a red flag:
// DON'T DO THISfunction Component({ value }) { const [derivedValue, setDerivedValue] = useState(null);
useEffect(() => { setDerivedValue(transform(value)); }, [value]);
return <div>{derivedValue}</div>;}The signal: useEffect with setState inside, where the state is derived from dependencies.
This creates:
- Extra state to manage
- Additional re-renders
- Potential for stale state
- More complex mental model
The Better Approach
// DO THIS INSTEADfunction Component({ value }) { const derivedValue = transform(value); return <div>{derivedValue}</div>;}Performance Considerations
You might worry about computing values on every render. For expensive calculations, use useMemo:
function ExpensiveList({ items, filter }) { // Memoize expensive filtering/sorting const processedItems = useMemo(() => { return items .filter(item => item.matches(filter)) .sort((a, b) => a.name.localeCompare(b.name)); }, [items, filter]);
return <List items={processedItems} />;}A Real Example: Form Validation
Before (with useEffect):
function SignupForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); const [isValid, setIsValid] = useState(false);
useEffect(() => { const newErrors = {}; if (!email.includes('@')) newErrors.email = 'Invalid email'; if (password.length < 8) newErrors.password = 'Too short';
setErrors(newErrors); setIsValid(Object.keys(newErrors).length === 0); }, [email, password]);
return ( <form> <input value={email} onChange={e => setEmail(e.target.value)} /> {errors.email && <span>{errors.email}</span>} <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> {errors.password && <span>{errors.password}</span>} <button disabled={!isValid}>Submit</button> </form> );}After (with derived state):
function SignupForm() { const [email, setEmail] = useState(''); const [password, setPassword] = useState('');
// Derived values const errors = { email: !email.includes('@') ? 'Invalid email' : null, password: password.length < 8 ? 'Too short' : null, }; const isValid = !errors.email && !errors.password;
return ( <form> <input value={email} onChange={e => setEmail(e.target.value)} /> {errors.email && <span>{errors.email}</span>} <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> {errors.password && <span>{errors.password}</span>} <button disabled={!isValid}>Submit</button> </form> );}No effects. No extra state. The validation is always in sync with the input values.
Summary
The rule is simple: use derived state when you can calculate a value from existing props or state during render. Use useEffect only for side effects that must happen after render.
If you find yourself writing:
useEffect(() => { setState(derivedValue);}, [dependency]);Stop. Ask: “Can I just compute this value directly?”
The answer is almost always yes.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 React Documentation - You Might Not Need an Effect
- 👨💻 Reddit Discussion - Derived State vs useEffect
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments