React Hooks for Interviews
Master React hooks for interviews, covering key hooks like useState, useEffect, useContext, common pitfalls, and best practices for building reusable logic
React hooks are special functions that start with use
. Introduced in React 16.8, they allow developers to use state and lifecycle features in function components. This simplifies code organization, reduce boilerplate, and enable better logic reuse. This guide will help you to tackle React hooks interview questions.
Familiarize yourself with the common hooks: useState
, useEffect
, useContext
, useRef
, useId
, you will probably have to use some of them during interviews.
useState
useState
enables function components to track state within component instances. State is isolated and private. Use useState
when a component needs local state that is contained within the component instance and not meant to be shared with siblings/parents. If you render a component in two separate places, each instance gets its own state.
const [state, setState] = useState(initialState);
Parameters
initialState
(Required): The initial value of the state. If it is a function, that function will be called to lazily initialize the state.
When updating based on the previous state, use the function version of setState
:
import { useState } from 'react';function Counter() {const [count, setCount] = useState(0);return (<button onClick={() => setCount((prevCount) => prevCount + 1)}>Count: {count}</button>);}
Pitfalls:
- Directly mutating state instead of using the setter function (
setState
) or not passing a new object to the setter function - Referencing stale values in closures and updating with stale values; use the function version of
setState
if possible - Using
useState
when a value doesn't affect rendering; useuseRef
instead
Further reading on react.dev: useState - React
Common mistake: mutating state directly
const [user, setUser] = useState({ name: 'Alice', age: 25 });user.age = 26; // ❌ This won't trigger a re-render
It is also not a good practice to re-use state objects:
function onClick() {user.age = 26;setUser(user); // ❌ Calling setter with the same object}
Instead, create a new object:
setUser((prevUser) => ({ ...prevUser, age: 26 }));
Common mistake: updating state without considering previous state
setCount(count + 1); // Potential stale state issue if used within intervals or async functions
One way to fix this is to use functional version of setter if your new state relies on the previous state.
setCount((prevCount) => prevCount + 1);
useEffect
useEffect
is a hook primarily for synchronizing a component with an external system. It can be used for executing side effects, such as fetching data, subscribing to events, or interacting with the DOM.
useEffect(effectFunction, dependencyArray);
Parameters
effectFunction
(Required): A function that contains the side effect logicdependencies
(Optional): An array of values that determine when the effect runs
Depending on when the effect should run, the dependencies
array is defined accordingly:
When should the effect run? | Dependency array | Example |
---|---|---|
After every render | None | useEffect(() => {...}); |
Once on mount | [] (empty) | useEffect(() => {...}, []); |
When any dependency changes | [var1, var2] | useEffect(() => {...}, [var1, var2]); |
Cleaning up (runs on unmount or before effect re-runs) | Varies | useEffect(() => { return () => {...}; }, []); |
useEffect
is primarily for side effects / interacting with external services and that isn't too common during interviews. If you find yourself reaching for useEffect
in your solutions, consider if there are alternatives.
import { useState, useEffect } from 'react';function DataFetcher() {const [data, setData] = useState(null);useEffect(() => {fetch('/api/data').then((response) => response.json()).then(setData);}, []);return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;}
Pitfalls:
- Not adding dependencies in the dependency array leading to stale closures
- Causing infinite loops by updating state inside
useEffect
without proper dependency management - Unnecessary re-renders due to using objects/arrays in the dependency array
- Not cleaning up side effects such as clearing timers and unsubscribing from events
It's a good practice to provide a dependency array in useEffect
, useCallback
, and useMemo
to avoid unexpected behavior. However in future the React compiler aims to remove the need for writing useCallback
and useMemo
manually.
Read the manual
The unfortunate truth is that useEffect
is just full of footguns and is hard to use, even for experienced engineers.
The useEffect
hook is especially hard to get right, even for seasoned developers. Our recommendation is to avoid using useEffect
in interviews if there are alternatives.
Given that interview questions are mostly self-contained, useEffect
s aren't too commonly needed. Hence you will find that the official solutions by GreatFrontEnd's React user interface questions do not have too much useEffect
usage. If you are using useEffect
in your code, compare with the official solutions to see how you can possibly avoid using it.
Either way, it's best to read the following pages of the React documentation to understand useEffect
better:
- Synchronizing with Effects
- You Might Not Need an Effect
- Lifecycle of Reactive Effects
- Separating Events from Effects
- Removing Effect Dependencies
Further reading on react.dev: useEffect - React
Common mistake: missing dependencies leading to stale closures
useEffect(() => {fetchData();}, []); // What if fetchData depends on props?
Ensure dependencies are correct, or use a callback inside useEffect
:
useEffect(() => {fetchData(someProp);}, [someProp]);
Common mistake: unnecessary effect invocations due to object/array dependency in useEffect
In the following example the effect should only run when filteredTodos
change, but effect still runs when the "Force re-render" button is triggered. This is because a new filteredTodos
array is recreated on every render, causing useEffect
to run unnecessarily.
function TodoList({ todos }) {const [count, setCount] = useState(0);const [status, setStatus] = useState('in_progress');const filteredTodos = todos.filter((todo) => todo.status === status);useEffect(() => {console.log('filteredTodos have changed');}, [filteredTodos]);return (<div><button onClick={() => setCount(count + 1)}>Force re-render</button><button onClick={() => setStatus('complete')}>Change status</button></div>);}
Use useMemo
to memoize the object, ensuring that filteredTodos
retain the same reference unless the todos
or status
change. Even though todos
is an array, it is a prop received from the parent and its still the same object reference when TodoList
re-renders.
In the following example, triggering the "Force re-render" button will not cause the effect to re-run.
function TodoList({ todos }) {const [count, setCount] = useState(0);const [status, setStatus] = useState('in_progress');const filteredTodos = useMemo(() => todos.filter((todo) => todo.status === status),[todos, status],);useEffect(() => {console.log('filteredTodos have changed');}, [filteredTodos]);return (<div><button onClick={() => setCount(count + 1)}>Force re-render</button><button onClick={() => setStatus('complete')}>Change status</button></div>);}
Common mistake: memory leaks due to missing cleanup
useEffect(() => {const interval = setInterval(() => {console.log('Running...');}, 1000);}, []); // No cleanup
Return a cleanup function when needed:
useEffect(() => {const interval = setInterval(() => {console.log('Running...');}, 1000);return () => clearInterval(interval);}, []);
useContext
useContext
provides a way to share state across components without prop drilling. It's useful for global state like themes, authentication, or user settings.
import { createContext, useContext, useState } from 'react';const ThemeContext = createContext('light');function App() {const [theme, setTheme] = useState('light');return (<ThemeContext.Provider value={theme}><buttononClick={() => {setTheme(theme === 'light' ? 'dark' : 'light');}}>Toggle theme</button><ThemeComponent /></ThemeContext.Provider>);}function ThemedComponent() {const theme = useContext(ThemeContext);return (<div style={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }}>Theme: {theme}</div>);}
Note that in React 19, Context providers can use <ThemeContext>
directly without doing <ThemeContext.Provider>
.
React automatically re-renders all the children that use a particular context starting from the provider that receives a different value.
Pitfalls:
- Using objects/functions as context values without memoization, leading to unnecessary re-renders
- Overusing
useContext
for deeply nested state that might be better managed with a state management library
Further reading on react.dev: useContext - React
Common mistake: using objects/functions as context values without memoization
Here the context value is a list of todos. Whenever TodoListContainer
re-renders (for example, on a route update), this will be a new array created thanks to the filtering, so React will also have to re-render all components deep in the tree that call useContext(TodoListContext)
.
filteredTodos
is recomputed on every render and creates a new array instance, even if todos
or status
haven't changed. This can cause unnecessary re-renders.
function TodoListContainer() {const [todos, setTodos] = useState([{ title: 'Fix bug', status: 'in_progress' },{ title: 'Add button', status: 'completed' },]);const [status, setStatus] = useState('in_progress');const filteredTodos = todos.filter((todo) => todo.status === status);return (<TodoListContext.Provider value={filteredTodos}><TodoList /></TodoListContext.Provider>);}
We can use useMemo
to memoize filteredTodos
, so it's only recalculated when todos
or status
change. This optimization can be significant for large lists.
function TodoListContainer() {const [todos, setTodos] = useState([{ title: 'Fix bug', status: 'in_progress' },{ title: 'Add button', status: 'completed' },]);const [status, setStatus] = useState('in_progress');const filteredTodos = useMemo(() => todos.filter((todo) => todo.status === status),[todos, status],);return (<TodoListContext.Provider value={filteredTodos}><TodoList /></TodoListContext.Provider>);}
useRef
useRef
stores a mutable reference that persists across renders, commonly used for:
- References data that is specific to the component instance
- Storing references to the DOM
- Storing data that does not affect the UI of the component (e.g. timeout or interval IDs). Mutating the ref values does not cause re-renders
import { useRef } from 'react';function FocusInput() {const inputRef = useRef(null);return (<div><input ref={inputRef} /><button onClick={() => inputRef.current.focus()}>Focus Input</button></div>);}
Pitfalls:
- Overusing
useRef
for state instead ofuseState
Further reading on react.dev: useRef - React
useId
useId
is a hook introduced in React 18 that generates unique IDs for accessibility attributes and form elements. It ensures that the IDs are unique within a component tree, even when server-rendering.
import { useId } from 'react';function Form() {const id = useId();return (<div><label htmlFor={id}>Name:</label><input id={id} type="text" /></div>);}
- Primarily design system components where IDs are used for accessibility attributes (like
aria-labelledby
) yet they need to have unique values because there can be multiple component instances on the page - Generating unique IDs for form inputs, especially in server-rendered applications
- Avoiding having to manually generating unique IDs with libraries or global counters
Pitfalls:
- Do not use
useId
for keys in lists. Use stable values like database IDs instead - It is only useful for generating static IDs and should not be used to track dynamic state
Further reading on react.dev: useId - React
Rules of hooks
React hooks follow a strict set of rules to ensure they function correctly and maintain state consistency across renders. Violating these rules can lead to bugs, unintended behavior, or broken components.
- Only call hooks at the top level: Do not call hooks inside loops, conditions, nested functions, or
try
/catch
/finally
blocks - Only call hooks from React function components or custom hooks: Avoid using hooks in regular JavaScript functions
Examples of incorrect vs correct hooks usage:
Do not call hooks inside conditions
❌ Wrong: Call hooks inside a condition
if (someCondition) {// ❌ Wrong: Hook inside a conditionconst [count, setCount] = useState(0);}
✅ Correct: Call hooks at the top level
const [count, setCount] = useState(0);if (someCondition) {console.log(count);}
Hooks must be called in the same order on every render. Conditional hooks may cause React to misalign state updates.
Only call hooks within React components or custom hooks
❌ Wrong: Call a hook outside a component
// ❌ Wrong: Outside of the componentconst count = useState(0);function App() {// ...}
✅ Correct: Call hooks inside functional components or custom hooks
function App() {const [count, setCount] = useState(0);return <p>{count}</p>;}function useCounter() {const [count, setCount] = useState(0);return [count, () => setCount((x) => x + 1)];}
Hooks rely on React's render cycle and component state. Calling hooks outside a component prevents React from tracking state.
Do not call hooks after a conditional return
❌ Wrong: Call hooks conditionally
function Counter({ hidden }) {if (hidden) {return;}// ❌ Wrong: After a conditional returnconst [count, setCount] = useState(0);// ...}
✅ Correct: Call hooks without a condition
function Counter({ hidden }) {const [count, setCount] = useState(0);if (hidden) {return;}// ...}
Do not calling hooks within loops
❌ Wrong: Call hooks inside a loop
for (let i = 0; i < 2; i++) {// ❌ Wrong: Hook inside a loopconst [count, setCount] = useState(0);}
✅ Correct: Call hooks without a loop
const [count1, setCount1] = useState(0);const [count2, setCount2] = useState(0);
React assumes hooks are called in the same order every render. Calling them in a different order or different number of calls can break React's internal state tracking.
Do not call in event handlers
❌ Wrong: Call hooks inside an event handler
function Counter() {function handleClick() {// ❌ Wrong: Inside an event handlerconst theme = useContext(ThemeContext);}// ...}
✅ Correct: Call hooks outside event handler
function Counter() {const theme = useContext(ThemeContext);function handleClick() {// ...}// ...}
Further reading on react.dev: Rules of Hooks
Custom hooks
Custom hooks in React allow you to extract reusable logic from components while maintaining state and side effects using built-in hooks like useState
, useEffect
, useMemo
, and others.
They follow the same rules as React hooks but enable better code reuse, abstraction, and separation of concerns in a clean and maintainable way.
- Code reusability: Instead of duplicating logic across components, a custom Hook encapsulates the logic and makes it reusable
- Separation of concerns: Components should focus on UI rendering, while custom hooks handle logic (state management, fetching data, event listeners, etc.)
- Cleaner & more readable components: Extracting logic into a Hook makes components less cluttered and more focused on presentation
- Encapsulation of side effects: Custom hooks allow managing side effects (like API calls) separately, making debugging and testing easier
A custom hook:
- Is a JavaScript function that starts with
use
(e.g.,useCounter
,useFetch
) - Must call other React hooks (e.g.,
useState
,useEffect
). If a custom hook does not call any React hooks, then it doesn't need to be a hook - Keep them focused – one purpose per hook
- Optionally, returns state or functions that components can use
Here's an example of a useCounter
custom hook. The value of such a hook is that it does not expose the setCount
function and users of the hook are restricted to the exposed operations, they can only increment, decrement, or reset the value.
import { useState } from 'react';function useCounter(initialValue = 0) {const [count, setCount] = useState(initialValue);const increment = () => setCount(count + 1);const decrement = () => setCount(count - 1);const reset = () => setCount(initialValue);return { count, increment, decrement, reset };}function CounterComponent() {const { count, increment, decrement, reset } = useCounter(10);return (<div><p>Count: {count}</p><button onClick={increment}>+</button><button onClick={decrement}>-</button><button onClick={reset}>Reset</button></div>);}
Further reading on react.dev: Reusing Logic with Custom Hooks
What you need to know for interviews
- Explain the purpose of hooks: Why React introduced hooks and how they replace class component lifecycle methods
- Know the common hooks: Understand when and why to use
useState
,useEffect
,useContext
, anduseRef
.useState
: Use the functional update form ofsetState
when state depends on the previous state e.g.setCount((prevCount) => prevCount + 1)
. This also eliminates the problem of stale closures within the event handlers
- Understand re-rendering issues: Recognize how dependency arrays impact performance and how
useMemo
/useCallback
optimize rendering - Implement a custom hook: Be able to write and explain a reusable custom hook in an interview
- Debug common hook pitfalls: Identify and fix common mistakes like missing dependencies in
useEffect
or unnecessary state updates causing re-renders
Practice questions
Quiz:
- What are the benefits of using hooks in React?
- What are the rules of React hooks?
- What is the difference between
useEffect
anduseLayoutEffect
in React? - What is the purpose of callback function argument format of
setState()
in React and when should it be used? - What does the dependency array of
useEffect
affect? - What is the
useRef
hook in React and when should it be used? - What is the
useCallback
hook in React and when should it be used? - What is the
useMemo
hook in React and when should it be used? - What is the
useReducer
hook in React and when should it be used? - What is the
useId
hook in React and when should it be used? - What is
forwardRef()
in React used for?
Coding: