Useful Hooks in React
React hooks are functions that allow you to use state and other React features in functional components. Here are the most useful hooks for building React applications.
Table of Contents
- Useful Hooks in React
What are React Hooks?
React hooks are functions that allow functional components to use state, lifecycle methods, and other React features that were previously only available in class components.
Key Concepts:
- State Management: useState for local state
- Side Effects: useEffect for lifecycle and side effects
- Performance: useMemo and useCallback for optimization
- Custom Logic: Custom hooks for reusable logic
Rules of Hooks:
// Good: Hooks at the top level
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <div>{count}</div>;
}
// Bad: Hooks inside conditions
function BadComponent() {
const [count, setCount] = useState(0);
if (count > 0) {
useEffect(() => {
// This breaks the rules of hooks
});
}
return <div>{count}</div>;
}
Core Hooks
1. useState Hook:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '', email: '' });
const [items, setItems] = useState([]);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const updateUser = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
const addItem = (item) => {
setItems(prevItems => [...prevItems, item]);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>Increment</button>
<input
value={user.name}
onChange={(e) => updateUser('name', e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser('email', e.target.value)}
placeholder="Email"
/>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
2. useEffect Hook:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch user data on mount and when userId changes
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Dependency array
// Update document title when user changes
useEffect(() => {
if (user) {
document.title = `${user.name}'s Profile`;
}
// Cleanup function
return () => {
document.title = 'React App';
};
}, [user]);
// Event listener effect
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty dependency array = run only on mount
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
3. useRef Hook:
import React, { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const countRef = useRef(0);
useEffect(() => {
// Focus input on mount
inputRef.current.focus();
}, []);
const handleClick = () => {
// Focus input on button click
inputRef.current.focus();
// Update ref without causing re-render
countRef.current += 1;
console.log('Click count:', countRef.current);
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Focus me" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}
// useRef for DOM measurements
function MeasurableComponent() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (divRef.current) {
const rect = divRef.current.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height
});
}
}, []);
return (
<div ref={divRef} style={{ padding: '20px', border: '1px solid black' }}>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
);
}
4. useCallback Hook:
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
// Stable function reference
const handleAddItem = useCallback((item) => {
setItems(prev => [...prev, item]);
}, []); // Empty dependency array = function never changes
// Function that depends on count
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies needed since we use functional update
// Function that depends on external value
const handleFilter = useCallback((filterValue) => {
setItems(prev => prev.filter(item => item.includes(filterValue)));
}, []); // No dependencies since we use functional update
return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
<ChildComponent
onAddItem={handleAddItem}
onFilter={handleFilter}
items={items}
/>
</div>
);
}
// Child component that receives stable callbacks
const ChildComponent = React.memo(({ onAddItem, onFilter, items }) => {
const [inputValue, setInputValue] = useState('');
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add item"
/>
<button onClick={() => {
onAddItem(inputValue);
setInputValue('');
}}>
Add Item
</button>
<input
placeholder="Filter items"
onChange={(e) => onFilter(e.target.value)}
/>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
});
5. useMemo Hook:
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ data, filter }) {
// Memoize expensive calculation
const processedData = useMemo(() => {
console.log('Processing data...');
return data
.filter(item => item.name.includes(filter))
.sort((a, b) => a.name.localeCompare(b.name))
.map(item => ({
...item,
processed: item.name.toUpperCase()
}));
}, [data, filter]); // Recalculate when data or filter changes
// Memoize complex object
const config = useMemo(() => ({
theme: 'dark',
language: 'en',
features: ['feature1', 'feature2']
}), []); // Empty array = object never changes
return (
<div>
<h3>Processed Items: {processedData.length}</h3>
<ul>
{processedData.map(item => (
<li key={item.id}>{item.processed}</li>
))}
</ul>
</div>
);
}
// useMemo for expensive computations
function FibonacciCalculator({ n }) {
const fibonacci = useMemo(() => {
console.log('Calculating fibonacci...');
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}, [n]); // Recalculate only when n changes
return (
<div>
<p>Fibonacci({n}) = {fibonacci}</p>
</div>
);
}
Custom Hooks
1. useLocalStorage Hook:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
// Update localStorage when value changes
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error writing to localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<div className={`app ${theme}`}>
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
</div>
);
}
2. useFetch Hook:
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
3. useDebounce Hook:
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const [results, setResults] = useState([]);
useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm).then(setResults);
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
4. usePrevious Hook:
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Advanced Hook Patterns
1. useReducer Hook:
import React, { useReducer } from 'react';
// Action types
const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
SET_VALUE: 'SET_VALUE'
};
// Reducer function
function counterReducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { ...state, count: state.count + 1 };
case ACTIONS.DECREMENT:
return { ...state, count: state.count - 1 };
case ACTIONS.RESET:
return { ...state, count: 0 };
case ACTIONS.SET_VALUE:
return { ...state, count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<h2>Count: {state.count}</h2>
<button onClick={() => dispatch({ type: ACTIONS.INCREMENT })}>
Increment
</button>
<button onClick={() => dispatch({ type: ACTIONS.DECREMENT })}>
Decrement
</button>
<button onClick={() => dispatch({ type: ACTIONS.RESET })}>
Reset
</button>
<button onClick={() => dispatch({ type: ACTIONS.SET_VALUE, payload: 10 })}>
Set to 10
</button>
</div>
);
}
2. useImperativeHandle Hook:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
select: () => {
inputRef.current.select();
}
}));
return <input ref={inputRef} {...props} />;
});
// Usage
function ParentComponent() {
const inputRef = useRef();
return (
<div>
<FancyInput ref={inputRef} placeholder="Fancy input" />
<button onClick={() => inputRef.current.focus()}>
Focus Input
</button>
<button onClick={() => inputRef.current.select()}>
Select All
</button>
</div>
);
}
3. useLayoutEffect Hook:
import React, { useState, useLayoutEffect, useRef } from 'react';
function Tooltip({ children, text }) {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef();
const tooltipRef = useRef();
useLayoutEffect(() => {
if (show && triggerRef.current && tooltipRef.current) {
const triggerRect = triggerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// Calculate position to center tooltip above trigger
const top = triggerRect.top - tooltipRect.height - 10;
const left = triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2);
setPosition({ top, left });
}
}, [show]);
return (
<div
ref={triggerRef}
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
>
{children}
{show && (
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
backgroundColor: 'black',
color: 'white',
padding: '5px',
borderRadius: '3px',
zIndex: 1000
}}
>
{text}
</div>
)}
</div>
);
}
Best Practices
1. Use Hooks at the Top Level:
// Good: Hooks at the top level
function GoodComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <div>{count}</div>;
}
// Bad: Hooks inside conditions or loops
function BadComponent() {
const [count, setCount] = useState(0);
if (count > 0) {
useEffect(() => {
// This breaks the rules of hooks
});
}
return <div>{count}</div>;
}
2. Use Dependency Arrays Correctly:
// Good: Include all dependencies
function GoodComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId);
}, [userId]); // Include userId in dependencies
return <div>{user?.name}</div>;
}
// Bad: Missing dependencies
function BadComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId);
}, []); // Missing userId dependency
}
3. Clean Up Side Effects:
// Good: Clean up side effects
function GoodComponent() {
useEffect(() => {
const interval = setInterval(() => {
console.log('Tick');
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>Component</div>;
}
// Bad: No cleanup
function BadComponent() {
useEffect(() => {
setInterval(() => {
console.log('Tick');
}, 1000);
}, []); // Memory leak!
}
4. Use Custom Hooks for Reusable Logic:
// Good: Custom hook for reusable logic
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Usage
function Counter1() {
const { count, increment, decrement } = useCounter(0);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
function Counter2() {
const { count, increment, reset } = useCounter(10);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Common Interview Questions
Q: What are React hooks?
- Functions that allow functional components to use state, lifecycle methods, and other React features.
Q: What are the rules of hooks?
- Only call hooks at the top level, don’t call hooks inside loops, conditions, or nested functions.
Q: What’s the difference between useState and useReducer?
- useState is for simple state, useReducer is for complex state logic with multiple actions.
Q: When should you use useCallback?
- When passing callbacks to optimized child components that rely on reference equality.
Q: When should you use useMemo?
- For expensive calculations or when you want to prevent unnecessary re-computations.
Q: What’s the difference between useEffect and useLayoutEffect?
- useEffect runs after DOM updates, useLayoutEffect runs synchronously before DOM updates.
Q: How do you create custom hooks?
- Create a function that starts with “use” and calls other hooks inside it.
Q: What’s the purpose of useRef?
- To persist values between renders without causing re-renders, and to access DOM elements.
Q: How do you handle cleanup in useEffect?
- Return a cleanup function from useEffect that runs before the component unmounts or before the effect runs again.
Q: What are the benefits of using hooks?
- Better code reuse, easier testing, and more intuitive component logic compared to class components.
Summary
- React hooks enable functional components to use state and lifecycle features
- Core hooks include useState, useEffect, useRef, useCallback, and useMemo
- Custom hooks allow you to extract and reuse component logic
- Rules of hooks must be followed for proper functionality
- Best practices include proper cleanup, dependency arrays, and hook organization
- Advanced patterns include useReducer, useImperativeHandle, and useLayoutEffect
- Understanding hooks is essential for modern React development
Interview angle
- “Which hooks matter beyond the basics?” -
useReducerfor several related transitions,useTransitionto mark an update non-urgent so typing stays responsive,useDeferredValueto render an expensive list from a lagging value,useIdfor SSR-safe ids, anduseSyncExternalStorefor subscribing to a store outside React without tearing. - “When do you write a custom hook?” - when stateful logic is used in more than one component. It is a plain function whose name starts with
useand which calls other hooks; there is no magic. Extracting one that is used once is usually just indirection. - “What does
useSyncExternalStoresolve?” - tearing under concurrent rendering, where different parts of one render read different values from an external store. It is why every serious state library exposes it rather than a bareuseEffectsubscription. - “
useTransitionoruseDeferredValue?” -useTransitionwhen you own the state update and can wrap it;useDeferredValuewhen the value arrives as a prop and you cannot.