Introduction to React Hooks
React Hooks, introduced in React 16.8, revolutionized how we write React components. They allow you to use state and other React features without writing class components. Understanding hooks thoroughly is essential for any React developer interview.
Why Were Hooks Created?
Before diving into specific hooks, it's important to understand the problems they solve. Class components had several issues: complex components became hard to understand, reusing stateful logic between components was difficult, and classes confused both people and machines (with 'this' binding issues and optimization difficulties).
Hooks address these problems by letting you use React features in functional components, making code more reusable and easier to understand.
useState: Managing Component State
useState is the most fundamental hook. It allows functional components to have state.
import { useState } from 'react';
function Counter() {
// Declare a state variable 'count' with initial value 0
const [count, setCount] = useState(0);
// Functional update for when new state depends on previous
const increment = () => setCount(prevCount => prevCount + 1);
// Direct update when new state is independent
const reset = () => setCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Important useState Concepts:
Lazy initialization: For expensive initial state computations, pass a function to useState. This function runs only on the first render.
const [data, setData] = useState(() => {
return computeExpensiveInitialData();
});
State updates are asynchronous: React batches state updates for performance. If you need to update state based on previous state, always use the functional form.
Object state: Unlike class component's setState, useState doesn't merge objects. You must spread the previous state manually.
const [user, setUser] = useState({ name: '', email: '' });
// Wrong-overwrites email
setUser({ name: 'John' });
// Correct-preserves email
setUser(prev => ({...prev, name: 'John' }));
useEffect: Side Effects in Functional Components
useEffect handles side effects like data fetching, subscriptions, and DOM manipulation. It combines componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Reset state when userId changes
setLoading(true);
setError(null);
// Create abort controller for cleanup
const abortController = new AbortController();
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: abortController.signal
});
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}
fetchUser();
// Cleanup function - runs before next effect and on unmount
return () => {
abortController.abort();
};
}, [userId]); // Dependency array - effect runs when userId changes
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user?.name}</div>;
}
useEffect Dependency Array Rules:
Empty array []: Effect runs only on mount and cleanup on unmount.
No array: Effect runs after every render (rarely what you want).
With dependencies: Effect runs on mount and whenever dependencies change.
Common useEffect Pitfalls:
Missing dependencies: Always include all values from component scope that the effect uses. ESLint's exhaustive-deps rule helps catch this.
Object/array dependencies: Objects and arrays are compared by reference. Either move them inside the effect, memoize them, or extract primitive values.
Infinite loops: If an effect updates state that's in its dependency array, you'll create an infinite loop.
useContext: Sharing State Without Prop Drilling
useContext provides a way to pass data through the component tree without manually passing props at every level.
import { createContext, useContext, useState } from 'react';
// Create context with default value
const ThemeContext = createContext('light');
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for using theme context
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Component using the context
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}
useReducer: Complex State Logic
useReducer is an alternative to useState for complex state logic. It's especially useful when state updates depend on previous state or when you have multiple sub-values.
import { useReducer } from 'react';
const initialState = {
items: [],
total: 0,
loading: false,
error: null
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
const item = state.items.find(i => i.id === action.payload);
return {
...state,
items: state.items.filter(i => i.id !== action.payload),
total: state.total - (item?.price || 0)
};
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'CLEAR_CART':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(cartReducer, initialState);
const addItem = (item) => {
dispatch({ type: 'ADD_ITEM', payload: item });
};
const removeItem = (itemId) => {
dispatch({ type: 'REMOVE_ITEM', payload: itemId });
};
return (
<div>
<h2>Cart ({state.items.length} items)</h2>
<p>Total: ${state.total}</p>
{state.items.map(item => (
<div key={item.id}>
{item.name} - ${item.price}
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
))}
</div>
);
}
useMemo and useCallback: Performance Optimization
These hooks help prevent unnecessary recalculations and re-renders.
useMemo memoizes a computed value:
import { useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
// Only recalculates when items or filter change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
useCallback memoizes a function:
import { useCallback, memo } from 'react';
// Memoized child component
const Button = memo(({ onClick, children }) => {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Without useCallback, this function is recreated every render
// causing Button to re-render even when count doesn't change
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty deps because we use functional update
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<Button onClick={handleClick}>Increment</Button>
</div>
);
}
useRef: Mutable Values and DOM Access
useRef creates a mutable reference that persists across renders without causing re-renders when changed.
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
const renderCount = useRef(0);
// Focus input on mount
useEffect(() => {
inputRef.current.focus();
}, []);
// Track renders without causing re-render
useEffect(() => {
renderCount.current += 1;
});
return (
<div>
<input ref={inputRef} type="text" />
<p>Render count: {renderCount.current}</p>
</div>
);
}
Custom Hooks: Reusable Logic
Custom hooks let you extract component logic into reusable functions. They must start with "use" and can call other hooks.
// Custom hook for form handling
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const handleBlur = useCallback((e) => {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
return {
values,
errors,
touched,
handleChange,
handleBlur,
setErrors,
reset
};
}
// Using the custom hook
function SignupForm() {
const { values, handleChange, handleBlur, reset } = useForm({
email: '',
password: ''
});
const handleSubmit = (e) => {
e.preventDefault();
console.log('Submitting:', values);
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
<button type="submit">Sign Up</button>
<button type="button" onClick={reset}>Reset</button>
</form>
);
}
Rules of Hooks
Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions.
Only call hooks from React functions - Call them from functional components or custom hooks.
// ❌ Wrong - conditional hook call
function Component({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null); // Error!
}
}
// ✅ Correct - unconditional hook, conditional logic inside
function Component({ isLoggedIn }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (isLoggedIn) {
fetchUser().then(setUser);
}
}, [isLoggedIn]);
}
Interview Tips
When discussing hooks in interviews, emphasize your understanding of the underlying concepts, when to use each hook, and common pitfalls to avoid. Be prepared to write code on a whiteboard and explain your reasoning. Practice explaining the differences between similar hooks like useMemo and useCallback, and when optimization is actually necessary.
