Are your React components re-rendering for no reason? Or getting stale state inside effects? We've seen these issues countless times. At Meteora Web, we build React frontends paired with Laravel backends every day. This guide cuts the fluff: what each hook does, when to use it, and — more importantly — when to avoid it. Let's start with the real problem: most developers use hooks without understanding dependency arrays and memoization trade-offs.
useState — Reactive state, often misused
useState seems trivial, but 90% of bugs come from async updates and stale values.
Functional updates — always use them when new state depends on previous
// ❌ Wrong: lost update if clicked twice quickly
setCount(count + 1);
// ✅ Correct: always receives the latest value
setCount(prev => prev + 1);
Same applies to objects and arrays: never mutate, always create a new copy.
Lazy initial state — for expensive computations
const [data, setData] = useState(() => {
const saved = localStorage.getItem('myData');
return saved ? JSON.parse(saved) : [];
});
useEffect — Controlled side effects
Effects run after render. Wrong dependencies cause infinite loops or missed updates.
The dependency array — include everything you use inside
Skip one variable and you get a stale closure. Use the exhaustive-deps ESLint rule.
Cleanup — free resources
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer);
}, []);
Fetch pattern with cancellation
useEffect(() => {
let cancelled = false;
fetch('/api/data')
.then(res => res.json())
.then(data => { if (!cancelled) setData(data); });
return () => { cancelled = true; };
}, []);
useRef — Mutable container that doesn't cause re-renders
Two main uses:
DOM references
const inputRef = useRef(null);
// later: inputRef.current.focus();
Storing values without triggering renders
const prevCountRef = useRef();
useEffect(() => { prevCountRef.current = count; });
const prevCount = prevCountRef.current;
useMemo — Memoizing expensive values
Returns a cached value that recalculates only when dependencies change. Use for heavy computations like filtering large arrays or formatting data.
const filteredList = useMemo(
() => items.filter(item => item.active),
[items]
);
Don't overuse it: it adds memory overhead. Profile first, memoize second.
useCallback — Stable function references
Returns the same function instance unless dependencies change. Critical when passing callbacks to child components optimized with React.memo.
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
return ;
Without it, every parent render creates a new function reference, causing the child to re-render unnecessarily.
Putting It All Together
function SearchForm({ onSubmit }) {
const [query, setQuery] = useState('');
const inputRef = useRef(null);
const debouncedQuery = useDebounce(query, 300);
const handleSubmit = useCallback((e) => {
e.preventDefault();
onSubmit(debouncedQuery);
}, [debouncedQuery, onSubmit]);
const searchResults = useMemo(() => {
if (!debouncedQuery) return [];
return expensiveSearch(debouncedQuery);
}, [debouncedQuery]);
useEffect(() => {
if (query) inputRef.current?.focus();
}, [query]);
return (
);
}
Common Mistakes — The Blacklist
- Stale closure: using a variable inside useEffect without listing it in dependencies. Include it or use useRef if intentional.
- Objects/arrays as dependencies: React compares by reference. Create stable references with useMemo.
- Premature memoization: don't optimize before measuring. First make it work, then profile.
- Missing cleanup: subscriptions, timers, listeners — always clean up.
In a Nutshell — What to Do Now
- Audit your useEffect: check that all variables used inside are in the dependency array.
- Add cleanup for every subscription (setInterval, addEventListener, cancelled fetch).
- Replace direct useState updates with functional form when new state depends on old.
- Profile renders with React DevTools: where you see unnecessary re-renders, consider useMemo/useCallback on children with React.memo.
- Remove unused memoization: if it doesn't improve performance, delete it.
At Meteora Web, we believe in writing clean code first, optimizing second. Hooks are powerful when understood deeply. For more, check the official React documentation.
Sponsored Protocol