π₯ Stop Using useState for Everything in React β The Hidden Performance Trap No One Talks About!
If youβve been developing in React for a while, youβve probably used useState a lot... maybe too much. While useState is a powerful and fundamental React hook, overusing it β or using it for the wrong purposes β can actually degrade your app's performance and cause unnecessary re-renders.
In this deep dive, weβll explore better alternatives, expose hidden pitfalls of useState, and walk through code examples that prove why replacing it in the right scenarios is not only possible, but highly recommended.
β οΈ TL;DR: Stop using useState for global/scoped state that never needs component re-renders. Discover useRef, useReducer, and external state managers.
React is reactive. When a componentβs state changes, it re-renders. This is by design. But what if you have state that doesnβt need to trigger re-renders? Like:
If youβre storing data in useState, React assumes:
βOh, they probably want to re-render this component when the value changes.β
This leads to unintentional waste. π΅βπ«
When you want to store a value without causing re-renders, useRef is gold.
β Bad: Using useState
const [timerId, setTimerId] = useState(null);
useEffect(() => { const id = setInterval(() => console.log('tick'), 1000); setTimerId(id); return () => clearInterval(id); }, []);
π΄ Every setTimerId call triggers a re-render that brings no UI benefit.
β Good: Using useRef
const timerIdRef = useRef();
useEffect(() => { timerIdRef.current = setInterval(() => console.log('tick'), 1000); return () => clearInterval(timerIdRef.current); }, []);
β Cleaner. No unnecessary re-renders. Use useRef for persistent, mutable data that doesnβt need to trigger views.
If your component state is growing in complexity, stop juggling multiple useStates.
β Messy State Explosion
const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [email, setEmail] = useState('');
β Centralize with useReducer
const initialState = { firstName: '', lastName: '', email: '' };
function reducer(state, action) { return { ...state, [action.field]: action.value, }; }
const [state, dispatch] = useReducer(reducer, initialState);
<input value={state.firstName} onChange={e => dispatch({ field: 'firstName', value: e.target.value })} />
π§ useReducer allows predictability and better debugging especially as your form or logic grows.
For global or cross-component state, like user auth or theme status:
β Use external lightweight stores like Zustand:
npm install zustand
// store.js import create from 'zustand';
const useStore = create(set => ({ theme: 'light', toggleTheme: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' })) }));
// App.js const theme = useStore(state => state.theme); const toggleTheme = useStore(state => state.toggleTheme);
Zustand doesn't rely on Reactβs render cycle. It offers selective reactivity + blazing-fast updates.
Avoid passing fresh function or object references every render unless theyβre memoized.
// Wrong - triggers child re-render <MyComponent onClick={() => doSomething()} />
// Right const memoizedHandler = useCallback(() => doSomething(), []); <MyComponent onClick={memoizedHandler} />
React is not about forcibly using useState and useEffect everywhere. We have better tools now.
Case | Better Alternative |
---|---|
Persisting mutable data | useRef |
Complex or grouped state updates | useReducer |
Shared/global state | Zustand, Jotai, Redux |
One-off logic or flags | useRef / local vars |
Level-up your React apps π by using the right state tool for the job.
π§΅ Don't forget to share this article with that one dev who still uses 15 useStates in a form! π
Want a follow-up article with practical migration steps from useState to useReducer or Zustand? Drop a comment below or DM me on X (@reactDoctor)!
π If you need help building efficient user interfaces or state-driven apps β we offer frontend development services.
Information