Skip to main content

What problem do hooks solve in React?

React hooks are functions that let function components use state, side effects, and other React features without class syntax.

Theory

TL;DR

  • Before React 16.8, only class components could have state and lifecycle methods
  • Sharing stateful logic between classes required HOCs or render props, creating deeply nested component trees
  • Lifecycle methods scattered related code: setup in componentDidMount, cleanup in componentWillUnmount, updates in componentDidUpdate
  • this binding in classes is a frequent source of bugs
  • Hooks solve all three through plain functions in any function component

Class vs hook: side by side

jsx
// Class: setup and cleanup live in separate methods class ChatRoom extends React.Component { componentDidMount() { this.socket = openSocket(this.props.roomId); } componentWillUnmount() { this.socket.close(); } } // Hook: one effect keeps both together function ChatRoom({ roomId }) { useEffect(() => { const socket = openSocket(roomId); return () => socket.close(); // cleanup lives next to setup }, [roomId]); }

The class splits one concern across two methods. The hook keeps it in one place. That is the core idea.

Why class components did not scale

A class component with moderate complexity spread its logic across four locations: state in the constructor, subscriptions in componentDidMount, reactions to prop changes in componentDidUpdate, and teardown in componentWillUnmount. Four places, one concern.

The bigger problem was reuse. If two components needed the same subscription logic, there was no clean path. Developers reached for Higher-Order Components or render props. Both patterns work. But stack a few HOCs and React DevTools shows Connect(WithAuth(WithTheme(MyComponent))). That is wrapper hell.

One thing I've noticed in codebases that migrated from classes: the files get shorter, but more importantly the logic becomes traceable. You read a useEffect and see the full lifecycle of that concern in a few lines.

Custom hooks: where the problem actually gets solved

useState and useEffect are useful, but custom hooks are the real answer to reuse. A custom hook is a function whose name starts with use and which calls other hooks inside. Pull your subscription logic into useSubscription, your form state into useForm, your API calls into useFetch. Any component can call it. No HOC, no nesting, no extra layers in the tree.

Common mistakes

Missing the dependency array:

jsx
useEffect(() => { fetchUser(userId); }); // runs after every render - usually a bug useEffect(() => { fetchUser(userId); }, [userId]); // runs only when userId changes

Forgetting the cleanup function:

jsx
// Bug: listener accumulates on every render useEffect(() => { window.addEventListener('keydown', handleKey); }, []); // Fix useEffect(() => { window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, []);

Reading state right after setting it:

jsx
setCount(count + 1); console.log(count); // still old value - React batches updates

State updates are async. The new value is only available on the next render.

Follow-up questions

Q: Why can't hooks be called inside conditions or loops?
A: React tracks hooks by call order. If a hook is skipped in one render, the order shifts and React reads wrong state for every hook after it. The rule keeps call order stable across renders.

Q: What is a custom hook?
A: A function starting with use that calls other hooks inside. It extracts stateful logic into a reusable function without adding any layers to the component tree.

Q: What did teams use before hooks to share logic between components?
A: Mainly HOCs and render props. Both work but wrap components in extra layers. A custom hook does the same thing with a plain function call.

Q: Does useEffect with [] fully replace componentDidMount?
A: Almost. Both run after the first render. But useEffect runs after the browser paints, while componentDidMount ran after DOM updates and before paint. For layout measurements, useLayoutEffect is closer to the original behavior.

Examples

Counter: class vs hook

jsx
// Class: constructor, bind, and this required throughout class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; this.increment = this.increment.bind(this); // easy to forget } increment() { this.setState({ count: this.state.count + 1 }); } render() { return <button onClick={this.increment}>{this.state.count}</button>; } } // Hook: same behavior, no ceremony function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; }

The class needs a constructor, a bind call, and this everywhere. The function component with useState does the same in two lines.

Custom hook: reusable data fetching

jsx
// Extract fetch logic once, use it in any component function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; fetch(url) .then(res => res.json()) .then(result => { if (!cancelled) setData(result); }) .catch(err => { if (!cancelled) setError(err); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; // prevent update after unmount }, [url]); return { data, loading, error }; } // Any component gets all three states in one line function UserProfile({ userId }) { const { data: user, loading } = useFetch(`/api/users/${userId}`); if (loading) return <span>Loading...</span>; return <h1>{user.name}</h1>; }

The fetch logic lives once and tests once. Before hooks, teams either duplicated it in every component or wrapped it in a HOC. A custom hook does it without nesting anything.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?