React

What are the benefits of using 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.

The higher-order component (HOC) approach:

function withWindowWidth(WrappedComponent) {
return class extends React.Component {
state = { width: window.innerWidth };

componentDidMount() {
window.addEventListener('resize', this.handleResize);
}

componentWillUnmount() {
window.removeEventListener(
'resize',
this.handleResize
);
}

handleResize = () => {
this.setState({ width: window.innerWidth });
};

render() {
return (
<WrappedComponent
width={this.state.width}
{...this.props}
/>
);
}
};
}

const MyComponent = withWindowWidth(({ width }) => {
return <p>Window width: {width}</p>;
});

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.

The custom hook approach (with hooks):

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;
}

function MyComponent() {
const width = useWindowWidth();
return <p>Window width: {width}</p>;
}

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.subscribeToChat(this.props.roomId);
document.title = `Room: ${this.props.roomId}`;
}

componentDidUpdate(prevProps) {
if (prevProps.roomId !== this.props.roomId) {
this.unsubscribeFromChat(prevProps.roomId);
this.subscribeToChat(this.props.roomId);
document.title = `Room: ${this.props.roomId}`;
}
}

componentWillUnmount() {
this.unsubscribeFromChat(this.props.roomId);
}

// ...
}

With hooks, related logic stays together:

function ChatRoom({ roomId }) {
useEffect(() => {
subscribeToChat(roomId);
return () => unsubscribeFromChat(roomId);
}, [roomId]);

useEffect(() => {
document.title = `Room: ${roomId}`;
}, [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 Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
this.setState({ count: this.state.count + 1 });
}

render() {
return (
<button onClick={this.handleClick}>
{this.state.count}
</button>
);
}
}

With hooks, there's no this at all:

function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}

Less boilerplate, fewer places to make mistakes.

Custom hooks compose naturally

One of the most powerful things about hooks is how they compose. Since hooks are just functions that call other hooks, you can build complex behavior from simple pieces.

function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false;
setLoading(true);

fetch(url)
.then((res) => res.json())
.then((result) => {
if (!cancelled) {
setData(result);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});

return () => {
cancelled = true;
};
}, [url]);

return { data, loading, error };
}

function UserProfile({ userId }) {
const {
data: user,
loading,
error,
} = useApi(`/api/users/${userId}`);

if (loading) return <p>Loading...</p>;
if (error) return <p>Something went wrong.</p>;
return <h1>{user.name}</h1>;
}

The useApi 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.