Skip to content

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:

ProductList.js
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:

  1. Props change
  2. useEffect triggers
  3. State updates
  4. Component re-renders

I could do:

  1. Props change
  2. Compute derived value directly
  3. Render with that value

The Solution: Derived State

Here’s the cleaner version of my ProductList component:

ProductList.js
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:

  1. You can compute a value from existing props or state

    UserProfile.js
    function UserProfile({ user }) {
    // Derived: computed from props
    const displayName = user.nickname || user.email.split('@')[0];
    const isAdmin = user.role === 'admin';
    return <div>{displayName} {isAdmin && '(Admin)'}</div>;
    }
  2. You need to transform or filter data

    TodoList.js
    function TodoList({ todos, filter }) {
    // Derived: filtering existing data
    const visibleTodos = todos.filter(todo => {
    if (filter === 'completed') return todo.completed;
    if (filter === 'active') return !todo.completed;
    return true;
    });
    return <TodoItems items={visibleTodos} />;
    }
  3. You need to combine multiple pieces of state

    Cart.js
    function Cart({ items }) {
    // Derived: computed from items
    const 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:

  1. 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 />;
    }
  2. 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 size
    return () => window.removeEventListener('resize', handleResize);
    }, []);
    return <span>{size.width} x {size.height}</span>;
    }
  3. 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:

AntiPattern.js
// DON'T DO THIS
function 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

BetterApproach.js
// DO THIS INSTEAD
function 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:

ExpensiveCalculation.js
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):

FormValidationBad.js
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):

FormValidationGood.js
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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments