Bugbie

← Back to Blog

React

React Hooks Deep Dive: useState, useEffect, and Beyond

March 7, 2026 · 10 min read

React Hooks transformed how React applications are written when they were introduced in React 16.8. The shift from class components to functional components with hooks has simplified code dramatically, enabled better logic reuse, and made components more readable. But hooks have subtle rules and behaviours that trip up developers at every experience level. This guide goes beyond the basics to give you a deep understanding of how hooks actually work and how to use them correctly.

useState: More Than You Think

State updates in React are asynchronous and batched. When you call setState, React doesn't immediately update the state value — it schedules a re-render. If you call setState multiple times in the same synchronous block, React batches them into a single re-render. This is usually a performance win, but it means you can't read the updated state value immediately after calling the setter.

Always use the functional form of setState when the new state depends on the old state: setCount(prev => prev + 1) instead of setCount(count + 1). The functional form receives the guaranteed current state value, not a potentially stale closure value.

useEffect: The Dependency Array Is Critical

The dependency array in useEffect determines when the effect re-runs. An empty array [] means run once on mount. A filled array means re-run when any listed dependency changes. No array means re-run on every render (almost never what you want). ESLint's exhaustive-deps rule will warn you when you forget to include a dependency — treat every warning as a bug, not noise.

The cleanup function returned from useEffect runs before the effect re-runs and when the component unmounts. Always clean up subscriptions, event listeners, and timers in the cleanup function. Forgetting cleanup is the most common source of memory leaks in React applications.

useCallback and useMemo: Use Sparingly

Both hooks cache a value or function between renders. useMemo caches the result of an expensive calculation. useCallback caches a function reference, useful when passing callbacks to child components wrapped in React.memo. The performance cost of the cache itself is real — only use them when you've measured an actual performance problem, not preemptively.

Custom Hooks: The Real Power of Hooks

Custom hooks are functions that use built-in hooks and expose a clean API for a specific piece of logic. A useLocalStorage hook encapsulates reading and writing to localStorage. A useDebounce hook delays updates to a value. A useFetch hook manages loading, data, and error state for a network request. Custom hooks are the mechanism for true logic reuse in React — and they're far superior to the old approach of render props and higher-order components.

useReducer for Complex State

When component state has multiple sub-values that change together, or when the next state depends on the previous in complex ways, useReducer is cleaner than multiple useState calls. The reducer pattern — a pure function that takes state and an action, returns new state — makes state transitions explicit and easy to test in isolation.

Conclusion

Hooks are simple on the surface but have depth that takes real experience to master. Focus on internalising the rules of hooks, understanding the dependency array, and cleaning up effects. Start extracting custom hooks early — even before they're reused — to keep components focused and logic separate. The codebase that results is more readable, more testable, and more maintainable.