Why use hooks in React?
The Short Answer
Hooks let you use state, side effects, and other React features in function components — without writing classes. They make code simpler, keep related logic together, and give you a clean way to share stateful logic between components through custom hooks.
Before hooks, sharing stateful logic meant reaching for patterns like higher-order components and render props, which worked but made your component tree deeply nested and hard to follow. Hooks solved that.
The Problems Hooks Solved
To appreciate hooks, you need to understand what life was like before them. There were three big pain points.
Sharing stateful logic was messy
Let's say you wanted to track the window width in multiple components. Before hooks, you had two options — and both had issues.
In the HOC approach below, you create a wrapper component that manages the resize listener and passes the width down as a prop. Notice how much ceremony is involved — a class with state, lifecycle methods for setup and teardown, and a render method that just forwards props. Every component that needs the window width gets wrapped in another layer.
function withWindowWidth(WrappedComponent: React.ComponentType<any>) {
return class extends React.Component {
state = { width: window.innerWidth };
handleResize = () => this.setState({ width: window.innerWidth });
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
}
render() {
return <WrappedComponent {...this.props} width={this.state.width} />;
}
};
}
// Usage — wraps your component in another component
const EnhancedHeader = withWindowWidth(Header);
const EnhancedSidebar = withWindowWidth(Sidebar);
This works, but it wraps your component in another component. Use a few HOCs and you end up with "wrapper hell" — a tower of components in your React DevTools that make debugging painful.
With the custom hook approach, the same window-width tracking logic lives in a single function. Any component that needs the window width just calls useWindowWidth() — no wrapping, no nesting, no extra components in the tree. The setup and cleanup live together in one useEffect, and the hook returns a plain value you can use directly.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
// Usage — no wrappers, no nesting
function Header() {
const width = useWindowWidth();
return <header>{width > 768 ? 'Desktop' : 'Mobile'}</header>;
}
function Sidebar() {
const width = useWindowWidth();
return <aside>{width > 1024 && <FullMenu />}</aside>;
}
Same functionality, no wrapper components, no nesting. The logic lives in a function you call. It's straightforward.
Related logic was split across lifecycle methods
In class components, you organized code by when it ran, not by what it did. Setting up a subscription happened in componentDidMount, cleaning it up happened in componentWillUnmount, and updating it happened in componentDidUpdate. One logical concern was scattered across three methods.
class ChatRoom extends React.Component {
componentDidMount() {
this.subscription = chatAPI.subscribe(this.props.roomId);
document.title = `Room: ${this.props.roomId}`;
}
componentDidUpdate(prevProps) {
if (prevProps.roomId !== this.props.roomId) {
this.subscription.unsubscribe();
this.subscription = chatAPI.subscribe(this.props.roomId);
document.title = `Room: ${this.props.roomId}`;
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
}
With hooks, each concern gets its own useEffect that keeps setup and cleanup side by side. In the code below, the chat subscription and the document title are two separate effects — each one is self-contained and easy to understand in isolation, unlike the class version where both concerns were tangled across three lifecycle methods.
function ChatRoom({ roomId }: { roomId: string }) {
// Concern 1: Chat subscription (setup + cleanup together)
useEffect(() => {
const subscription = chatAPI.subscribe(roomId);
return () => subscription.unsubscribe();
}, [roomId]);
// Concern 2: Document title (setup + cleanup together)
useEffect(() => {
document.title = `Room: ${roomId}`;
return () => { document.title = 'Chat App'; };
}, [roomId]);
}
Each useEffect handles one concern from start to finish — setup and cleanup, side by side. You can read each effect independently and understand what it does.
The this keyword was confusing
Class components required you to bind event handlers or use arrow functions to get the right this context. It was a constant source of bugs, especially for developers newer to JavaScript.
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.state = { query: '' };
// Forget this line and handleChange breaks
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({ query: e.target.value });
}
render() {
return <input value={this.state.query} onChange={this.handleChange} />;
}
}
With hooks, the same component becomes a plain function — no constructor, no this, no binding. The state lives in a useState call and the event handler is just an inline arrow function. There's nothing to forget or get wrong.
function SearchBar() {
const [query, setQuery] = useState('');
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Less boilerplate, fewer places to make mistakes.
Custom Hooks Compose Naturally
One of the most powerful things about hooks is how they compose. In the code below, useFetch is a low-level hook that handles fetching, loading, and error states. Then useUser builds on top of it — it's just a thin wrapper that provides the URL. The component at the end calls useUser() and gets back everything it needs in one line, with all the complexity hidden inside the hooks.
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Higher-level hook built on useFetch
function useUser(userId: string) {
const { data, loading, error } = useFetch<User>(`/api/users/${userId}`);
return { user: data, loading, error };
}
// Component uses the composed hook
function Profile({ userId }: { userId: string }) {
const { user, loading, error } = useUser(userId);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserCard user={user} />;
}
The useUser hook encapsulates fetching, loading state, and error handling. Any component can use it. And unlike HOCs, using multiple custom hooks in the same component doesn't create nesting or naming collisions — each hook call is independent.
Simpler Code Overall
When you add it all up — no classes, no this binding, no lifecycle method juggling, no wrapper components — you get code that's shorter, easier to read, and easier to maintain. Function components with hooks have become the standard way to write React. Not because classes were bad, but because hooks solved real problems that classes couldn't address cleanly.
Why Interviewers Ask This
This question checks whether you understand the motivation behind hooks, not just how to use useState and useEffect. Interviewers want to hear that you know what problems existed before hooks (sharing logic, scattered lifecycle code, this confusion) and how hooks address each one. It shows you understand React's evolution and can articulate why the tools you use every day are designed the way they are.
Quick Revision Cheat Sheet
No classes needed: State and effects work in function components
Colocation: Related logic (setup + cleanup) stays together in one useEffect
No this: Function components use closures, eliminating binding bugs
Custom hooks: Share stateful logic as plain functions — no wrappers or nesting
Composable: Hooks are functions that call other hooks — build complex from simple