The Ultimate Guide to React Hooks
What Are React Hooks? Your Ultimate Interview Guide
Before React 16.8, if you needed to add state or use lifecycle methods (like componentDidMount) to a component, you had to use a Class Component. This led to complex components, hard-to-follow logic, and wrapper hell.
React Hooks were introduced to solve this. They are functions that let you 'hook into' React's state and lifecycle features from Function Components.
This means you can now have state, side effects, and more, all in a simple, clean function.
The Two Golden Rules of Hooks (A Top Interview Question)
You must tell your interviewer these rules. They are critical.
- Only Call Hooks at the Top Level: You cannot call Hooks inside loops, conditions (
ifstatements), or nested functions. They must be called at the top level of your React function. - Only Call Hooks from React Functions: You can only call Hooks from React Function Components or from your own Custom Hooks. You can't call them from regular JavaScript functions.
This rule ensures that Hooks are called in the same order on every render, which is how React keeps track of their state.
The Main Hooks to Know:
useState: For adding state to a component.useEffect: For performing side effects (data fetching, timers, DOM manipulation).useContext: For accessing a React Context (avoids prop drilling).useReducer: An alternative touseStatefor complex state logic.useMemo: For memoizing expensive calculations (a value).useCallback: For memoizing functions, often to prevent unnecessary re-renders in child components.
This guide will link to deep dives on each of these critical hooks.
useState vs. useReducer: When to Use Each in React
Interview Question: 'When would you use useReducer over useState?'
This is a common question to check if you understand how to manage complex state. Here's a simple way to answer.
1. useState: For Simple State
Use useState when your state is simple, like a number, string, boolean, or a simple object/array.
Best for:
- A counter
- A 'loading' boolean
- A form input's string value
Code Example: Simple Counter with useState
import React, { useState } from 'react';
function Counter() {
// Returns the value, and a setter function
const [count, setCount] = useState(0);
return (
Count: {count}
);
}2. useReducer: For Complex State
Use useReducer when your state logic is complex, or when the next state value depends on the previous one. It's also great when you have multiple state values that often change together (like in a complex form).
It takes a reducer function and initialState, and returns the state and a dispatch function.
Best for:
- Managing a shopping cart (add, remove, update quantity).
- Complex forms with many related fields.
- Any state where the next state is calculated from the previous state (like our counter!).
Code Example: Same Counter with useReducer
import React, { useReducer } from 'react';
// 1. The Reducer function: (currentState, action) => newState
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
// 2. Initialize the hook
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
Count: {state.count}
{/ 3. Dispatch actions instead of setting state /}
);
}The simple answer: 'I use useState for simple state. I use useReducer when state logic is complex or when multiple state values are tied together, as it makes the logic more predictable and easier to test.'
useEffect: How to Handle Data Fetching and Cleanup
Interview Question: 'How do you fetch data with useEffect?'
useEffect is the hook for all 'side effects'—tasks that are outside of the normal render, like API calls, timers, or DOM manipulations.
The Dependency Array is Key
The second argument to useEffect is the 'dependency array'. It controls when the effect runs.
[](Empty Array): The effect runs only once, when the component mounts. Perfect for initial data fetching.[prop, state](Has Values): The effect runs on mount AND any time the values ofproporstatechange.- No Array (Omitted): The effect runs after every single render. This is usually a bug and can cause infinite loops.
Code Example: Data Fetching
Here is how you correctly fetch data from an API when the component loads.
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// We must use an async function inside the effect
// because the effect function itself cannot be async.
async function fetchData() {
setLoading(true);
try {
const response = await fetch(`https.api.example.com/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error("Failed to fetch user", error);
}
setLoading(false);
}
fetchData();
}, [userId]); // <-- Dependency array: re-run if userId changes
if (loading) return Loading...
;
if (!user) return No user found.
;
return Username: {user.name}
;
}The Cleanup Function
An interviewer might ask, 'What does useEffect return?'
Answer: 'It can return an optional cleanup function. React will run this function when the component unmounts, or before the effect runs again. It's essential for preventing memory leaks.'
Code Example: Cleanup with a Timer
useEffect(() => {
// Side effect: start a timer
const timerId = setInterval(() => {
console.log('Timer ticked');
}, 1000);
// Cleanup function: runs on unmount
return () => {
console.log('Cleaning up the timer');
clearInterval(timerId);
};
}, []); // [] means this runs once on mount, cleans up on unmountuseCallback and useMemo: A Practical Guide to Performance
Interview Question: 'What's the difference between useMemo and useCallback?'
This is a performance-related question. Using them incorrectly can make performance worse. Here’s the simple difference.
useMemomemoizes a VALUE.useCallbackmemoizes a FUNCTION.
'Memoization' just means caching the result. React will skip re-calculating (or re-creating) the value/function if the dependencies haven't changed, and will just use the cached version.
1. useMemo: Caching Expensive Calculations
Use useMemo when you have a 'slow' calculation that you don't want to re-run on every render.
Code Example:
import React, { useState, useMemo } from 'react';
// A 'slow' function
function expensiveCalculation(num) {
console.log('Running slow calculation...');
// ... imagine this takes a long time ...
return num * 2;
}
function Calculator() {
const [count, setCount] = useState(10);
const [otherState, setOtherState] = useState(0);
// This calculated value is 'memoized'.
// It will ONLY re-run if 'count' changes.
// It will NOT re-run if 'otherState' changes.
const calculatedValue = useMemo(() => {
return expensiveCalculation(count);
}, [count]);
return (
Calculated Value: {calculatedValue}
Other State: {otherState}
);
}2. useCallback: Caching Functions
In JavaScript, a function is a new object on every render. This is a problem when you pass that function as a prop to a child component that is wrapped in React.memo(). The child will re-render just because it got a 'new' (but identical) function prop.
useCallback gives you the same function instance back, as long as its dependencies don't change.
Code Example:
import React, { useState, useCallback } from 'react';
// A child component that is memoized
const ChildComponent = React.memo(function Child({ onClick }) {
console.log('Child rendered');
return ;
});
function Parent() {
const [count, setCount] = useState(0);
// This function is memoized.
// React will return the same function instance every render.
// This prevents ChildComponent from re-rendering unnecessarily.
const handleClick = useCallback(() => {
console.log('Clicked!');
// Note: If we used 'count' here, we'd add it to the dependency array: [count]
}, []);
return (
Parent Count: {count}
);
}The simple answer: 'Use useMemo to cache the result of a slow calculation. Use useCallback to cache a function definition so you can pass a stable prop to a memoized child component.'
useContext: Ditching 'Prop Drilling' for Good
Interview Question: 'What is prop drilling and how do you solve it?'
This question tests your knowledge of basic React state management patterns.
What is Prop Drilling?
Prop Drilling is the term for when you have to pass props down through multiple layers of components, even when the components in the middle don't need or use those props. They just act as a 'pass-through'.
'Before' Code Example: Prop Drilling
Imagine a user's theme preference ('dark' or 'light') is needed by a Button component, but the state is held in the top-level App.
// App.js (Top Level)
function App() {
const [theme, setTheme] = useState('light');
return ;
}
// Toolbar.js (Middle Level - doesn't need theme)
function Toolbar({ theme }) {
// ... just passes it down ...
return ;
}
// ThemedButton.js (Deep Level - needs theme)
function ThemedButton({ theme }) {
return ;
}This is messy. If another component 10 levels deep needed theme, you'd have to 'drill' it all the way down.
How useContext Solves It
The Context API lets you create a 'global' state for a branch of your component tree. Any child can 'subscribe' to it directly without props.
It has 3 parts:
React.createContext(): Creates the context object.MyContext.Provider: The component that 'provides' the value to all children.useContext(MyContext): The Hook that 'consumes' the value in any child component.
'After' Code Example: Using useContext
import React, { useState, useContext, createContext } from 'react';
// 1. Create the context
const ThemeContext = createContext('light'); // 'light' is the default
// App.js (Top Level)
function App() {
const [theme, setTheme] = useState('light');
// 2. Provide the value to all children
return (
);
}
// Toolbar.js (Middle Level - now it's clean!)
function Toolbar() {
// ... doesn't even know about 'theme' ...
return ;
}
// ThemedButton.js (Deep Level)
function ThemedButton() {
// 3. Consume the value directly!
const theme = useContext(ThemeContext);
return ;
}
The simple answer: 'Prop drilling is passing props down many levels. I solve it with the useContext Hook, which lets a component consume a value from a Provider far above it in the tree, without passing props through the middle components.'
How to Create Your Own Custom React Hooks
Interview Question: 'Have you ever built a Custom Hook? How?'
This is a great question to show you've moved beyond the basics. The answer is surprisingly simple.
Answer: 'A Custom Hook is just a regular JavaScript function whose name starts with use. Its purpose is to extract and reuse component logic (that includes other hooks)'.
Instead of having the same useState and useEffect logic intwo different components (e.g., to fetch data, or to track window size), you put that logic into a custom hook and just call that hook in both components.
Rules for Custom Hooks:
- Name MUST start with
use. This is how React knows it's a hook and can apply the Rules of Hooks. (e.g.,useWindowSize, notgetWindowSize). - It can (and should) call other hooks like
useState,useEffect, etc. - It should return the values the component needs (e.g., the state, or functions to change it).
Code Example: A useWindowSize Custom Hook
Let's make a hook that tracks the browser window's width and height. This is a classic example.
import { useState, useEffect } from 'react';
// 1. Create the hook (a function starting with 'use')
export function useWindowSize() {
// 2. Use other hooks (like useState) inside
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
// 3. Use other hooks (like useEffect) inside
useEffect(() => {
// This function updates the state
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Set up the side effect (event listener)
window.addEventListener('resize', handleResize);
// Call handleResize right away to get the current size
handleResize();
// 4. Return the cleanup function
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array = run only on mount and unmount
// 5. Return the value your component needs
return windowSize;
}
// --- NOW, IN ANY COMPONENT, YOU CAN USE IT ---
// MyComponent.js
import React from 'react';
import { useWindowSize } from './useWindowSize';
function MyComponent() {
// 6. Use your hook just like a built-in one!
const { width, height } = useWindowSize();
return (
Window width: {width}px
Window height: {height}px
);
}This is powerful because now any component can get the window size by calling useWindowSize(), and all the complex logic is hidden away and reusable.


