React Hooks Interview Questions (Free Preview)
Free sample of 15 from 56 questions available
useEffect Hook
What is the difference between useEffect with no dependency array, empty array, and array with values?
The 30-Second Answer:
No dependency array means the effect runs after every render. An empty array [] means it runs only once after initial mount. An array with values [a, b] means it runs when any of those values change between renders.
The 2-Minute Answer (If They Want More): These three variants of useEffect represent different synchronization strategies, each suited for different use cases:
No dependency array (useEffect(() => {})) runs after every single render, including the initial render and all re-renders caused by state updates, prop changes, or parent re-renders. This is rarely what you want because it can cause performance issues and infinite loops if the effect updates state. However, it's useful for debugging or when you genuinely need to synchronize with something on every render.
Empty dependency array (useEffect(() => {}, [])) runs only once after the component mounts, similar to componentDidMount in class components. This is perfect for one-time setup operations like initializing third-party libraries, fetching initial data that doesn't depend on props/state, or setting up subscriptions that don't need to change. The cleanup function (if provided) will only run when the component unmounts.
Array with values (useEffect(() => {}, [dep1, dep2])) runs after the initial render and whenever any of the specified dependencies change. This is the most common pattern and represents true synchronization - your effect stays in sync with specific values. The cleanup runs before each re-execution and on unmount, ensuring you clean up old subscriptions/timers before setting up new ones.
Code Example:
import { useState, useEffect } from 'react';
function ComparisonDemo({ userId, filter }) {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// 1. NO DEPENDENCY ARRAY
// Runs after EVERY render (initial + all re-renders)
useEffect(() => {
console.log('1. No deps: Runs every render');
console.log(`Count is now: ${count}, userId: ${userId}`);
}); // ⚠️ No second argument
// When does this run?
// - Initial render
// - When count changes
// - When userId changes
// - When filter changes
// - When parent re-renders
// - Any other state/prop change
// 2. EMPTY DEPENDENCY ARRAY
// Runs ONLY ONCE after initial mount
useEffect(() => {
console.log('2. Empty deps: Runs once on mount');
// Perfect for one-time initialization
const analytics = initializeAnalytics();
loadUserPreferences();
return () => {
console.log('2. Cleanup: Only on unmount');
analytics.shutdown();
};
}, []); // Empty array
// When does this run?
// - Initial render ONLY
// Cleanup runs when component unmounts ONLY
// 3. ARRAY WITH VALUES
// Runs on initial render + when dependencies change
useEffect(() => {
console.log('3. With deps: Runs when userId or filter changes');
fetchData(userId, filter).then(setData);
return () => {
console.log('3. Cleanup: Before next effect or unmount');
};
}, [userId, filter]); // Specific dependencies
// When does this run?
// - Initial render
// - When userId changes
// - When filter changes
// NOT when count changes
// NOT when unrelated props change
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<p>Data items: {data.length}</p>
</div>
);
}
// Real-world examples for each pattern:
// Example A: No dependency array (rarely used, but valid)
function ScrollLogger() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
// This effect needs to run on every render to log
// because we want to track every state change
console.log('Component rendered at scroll position:', scrollPosition);
}); // No array - intentionally runs every render
useEffect(() => {
const handleScroll = () => setScrollPosition(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Scroll position: {scrollPosition}</div>;
}
// Example B: Empty array (common for mount-only effects)
function ThirdPartyIntegration() {
useEffect(() => {
// Initialize library once
const map = L.map('map').setView([51.505, -0.09], 13);
// Add tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Cleanup on unmount
return () => {
map.remove();
};
}, []); // Only initialize once
return <div id="map" style={{ height: '400px' }} />;
}
// Example C: Dependency array (most common)
function SearchResults({ searchQuery, category }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Re-fetch when search parameters change
if (!searchQuery) {
setResults([]);
return;
}
setLoading(true);
const controller = new AbortController();
fetch(`/api/search?q=${searchQuery}&category=${category}`, {
signal: controller.signal
})
.then(res => res.json())
.then(data => {
setResults(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
setLoading(false);
}
});
// Cleanup: abort in-flight request if params change
return () => controller.abort();
}, [searchQuery, category]); // Re-run when search params change
return <div>{loading ? 'Loading...' : results.map(r => <div key={r.id}>{r.title}</div>)}</div>;
}
// Decision tree for choosing the right pattern:
function DecisionExample() {
// Ask yourself:
// Q: Does this effect need to run on every render?
// A: Yes → No dependency array (rare)
useEffect(() => {
// Example: logging every render for debugging
});
// Q: Does this effect only need to run once when component mounts?
// A: Yes → Empty dependency array
useEffect(() => {
// Example: initialize analytics, fetch static data
}, []);
// Q: Does this effect depend on props, state, or other values?
// A: Yes → Include those values in dependency array
useEffect(() => {
// Example: fetch data based on user input
}, [/* dependencies */]);
}
Execution Pattern Comparison:
graph TD
subgraph "No Dependency Array"
A1[Initial Render] --> A2[Run Effect]
A2 --> A3[State Update]
A3 --> A4[Run Cleanup]
A4 --> A5[Run Effect Again]
A5 --> A6[Any Change]
A6 --> A4
end
subgraph "Empty Array []"
B1[Initial Render] --> B2[Run Effect]
B2 --> B3[State Updates...]
B3 --> B3
B3 --> B4[Unmount]
B4 --> B5[Run Cleanup]
end
subgraph "With Dependencies [a, b]"
C1[Initial Render] --> C2[Run Effect]
C2 --> C3[State Update - Not a or b]
C3 --> C3
C3 --> C4[a or b Changes]
C4 --> C5[Run Cleanup]
C5 --> C6[Run Effect Again]
C6 --> C3
end
style A2 fill:#ffcccc
style A5 fill:#ffcccc
style B2 fill:#ccffcc
style C2 fill:#ccccff
style C6 fill:#ccccff
Quick Reference Table:
| Pattern | Syntax | Runs on mount? | Runs on re-render? | Cleanup timing |
|---|---|---|---|---|
| No deps | useEffect(() => {}) |
âś… Yes | âś… Always | Before every re-run + unmount |
| Empty array | useEffect(() => {}, []) |
✅ Yes | ❌ Never | On unmount only |
| With deps | useEffect(() => {}, [a, b]) |
âś… Yes | âś… When deps change | Before re-run + unmount |
References:
↑ Back to topWhat is the dependency array in useEffect and how does it work?
The 30-Second Answer: The dependency array is the optional second argument to useEffect that tells React when to re-run the effect. React compares each dependency with its previous value using Object.is comparison, and only re-runs the effect if any dependency has changed.
The 2-Minute Answer (If They Want More): The dependency array is React's optimization mechanism for controlling when effects execute. It's an array of values (props, state, or any variables from the component scope) that the effect depends on. React performs a shallow comparison between the current and previous values of each dependency, and only re-executes the effect if at least one value has changed.
Understanding how React compares dependencies is crucial. React uses Object.is comparison, which means primitives (numbers, strings, booleans) are compared by value, while objects, arrays, and functions are compared by reference. This is why inline object/array literals or function definitions in the dependency array cause effects to run on every render - they're new references each time.
The dependency array serves two purposes: performance optimization (avoiding unnecessary effect runs) and correctness (ensuring the effect uses fresh values). Omitting a dependency that your effect uses is a bug that can lead to stale closures - your effect will use outdated values. Modern linting tools (eslint-plugin-react-hooks) help catch these mistakes.
Every value used inside the effect that can change between renders should be in the dependency array. This includes props, state, and any variables derived from them. Constants defined outside the component and functions that don't use component scope don't need to be included.
Code Example:
import { useState, useEffect } from 'react';
function DependencyExamples({ userId, apiEndpoint }) {
const [user, setUser] = useState(null);
const [count, setCount] = useState(0);
// Example 1: Effect depends on userId
// Re-runs whenever userId changes
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Dependency array with one value
// Example 2: Effect depends on multiple values
// Re-runs when EITHER userId OR apiEndpoint changes
useEffect(() => {
const url = `${apiEndpoint}/users/${userId}`;
fetch(url).then(res => res.json()).then(setUser);
}, [userId, apiEndpoint]); // Multiple dependencies
// Example 3: Empty dependency array
// Only runs once after initial render (like componentDidMount)
useEffect(() => {
console.log('Component mounted');
}, []); // Empty array = run once
// Example 4: No dependency array
// Runs after EVERY render (usually not what you want)
useEffect(() => {
console.log('This runs after every render');
}); // No array = run every render
return <div>{user?.name}</div>;
}
// Common mistakes and how to fix them:
// ❌ WRONG: Object/array literal in dependency causes infinite loop
function BadExample1({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(userId, { cache: true }).then(setData);
}, [userId, { cache: true }]); // New object every render!
}
// âś… CORRECT: Use primitive values or useMemo
function GoodExample1({ userId }) {
const [data, setData] = useState(null);
const options = useMemo(() => ({ cache: true }), []); // Stable reference
useEffect(() => {
fetchData(userId, options).then(setData);
}, [userId, options]); // options reference is stable
}
// ❌ WRONG: Missing dependency (stale closure)
function BadExample2({ userId }) {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(`User ${userId}, Count: ${count}`); // Uses stale count!
}, 1000);
return () => clearInterval(timer);
}, [userId]); // Missing 'count' - will always log initial count value
}
// âś… CORRECT: Include all dependencies or use functional update
function GoodExample2({ userId }) {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => {
console.log(`User ${userId}, Count: ${c}`); // Uses current count
return c;
});
}, 1000);
return () => clearInterval(timer);
}, [userId]); // Don't need count in deps when using functional update
}
// Comparison behavior visualization
function ComparisonExample() {
const [primitive, setPrimitive] = useState(1);
const [obj, setObj] = useState({ id: 1 });
// Primitive comparison: by value
useEffect(() => {
console.log('Primitive changed');
}, [primitive]); // Runs when primitive value changes: 1 → 2
// Object comparison: by reference
useEffect(() => {
console.log('Object changed');
}, [obj]); // Runs when obj reference changes
const handleClick = () => {
setPrimitive(1); // No re-run: 1 === 1
setObj({ id: 1 }); // Re-runs: new object reference even with same values
};
return <button onClick={handleClick}>Update</button>;
}
Dependency Comparison Flow:
graph TD
A[Effect scheduled to run] --> B{Has dependency array?}
B -->|No array| C[Run effect every render]
B -->|Empty array []| D{Is this initial render?}
B -->|Has dependencies| E[Compare each dependency]
D -->|Yes| F[Run effect]
D -->|No| G[Skip effect]
E --> H{Any dependency changed?}
H -->|Yes - Object.is returns false| F
H -->|No - All same| G
F --> I[Run cleanup from previous effect]
I --> J[Run effect callback]
style C fill:#ffcccc
style F fill:#ccffcc
style G fill:#ffffcc
References:
↑ Back to topWhat is useEffect and when does it run?
The 30-Second Answer: useEffect is a React Hook that lets you synchronize a component with external systems (side effects) like APIs, timers, or the DOM. By default, it runs after every render, but you can control when it runs using the dependency array.
The 2-Minute Answer (If They Want More): useEffect is React's way of handling side effects in functional components. A side effect is any code that affects something outside the scope of the current function being executed - things like data fetching, subscriptions, manually changing the DOM, timers, or logging.
The hook runs after React has updated the DOM, ensuring your side effects don't block the browser from painting. This makes your app feel more responsive. useEffect executes after the first render and after every subsequent re-render by default, though this behavior can be customized with the dependency array.
The timing of useEffect is important: it runs asynchronously after the render is committed to the screen. This is different from componentDidMount/componentDidUpdate which run synchronously. If you need synchronous execution (rare cases like measuring DOM), you'd use useLayoutEffect instead.
Think of useEffect as a way to "step outside" of React's pure functional world to interact with the imperative world of side effects, while still maintaining React's declarative programming model.
Code Example:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// This effect runs after every render
useEffect(() => {
console.log('Effect ran!');
console.log('User ID:', userId);
// This code runs after the component renders
document.title = `User ${userId}'s Profile`;
});
return <div>User ID: {userId}</div>;
}
// Execution order:
// 1. Component renders
// 2. React updates the DOM
// 3. Browser paints the screen
// 4. useEffect callback runs
References:
↑ Back to topWhat is the cleanup function in useEffect and when is it called?
The 30-Second Answer: The cleanup function is returned from useEffect to clean up side effects like subscriptions, timers, or event listeners. It runs before the component unmounts and before the effect runs again on subsequent renders, preventing memory leaks and unwanted behavior.
The 2-Minute Answer (If They Want More): When your effect sets up something that needs to be torn down (like a subscription, timer, or event listener), you return a cleanup function from useEffect. This function is crucial for preventing memory leaks and ensuring your component doesn't cause side effects after it's no longer needed.
The cleanup function runs in two scenarios: (1) before the component unmounts (removed from the DOM), and (2) before the effect runs again due to dependency changes. This second point is important - if your effect depends on props or state that change, React will run your cleanup before running the new effect with updated values.
React guarantees that cleanup from the previous render completes before running the next effect. This prevents race conditions and ensures you don't have multiple subscriptions or listeners active at once. For example, if you're subscribing to a chat room, the cleanup ensures you unsubscribe from the old room before subscribing to a new one.
A common mistake is forgetting cleanup for subscriptions and event listeners, which can cause memory leaks, especially in single-page applications where components mount and unmount frequently without page refreshes.
Code Example:
import { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Setup: Subscribe to chat room
console.log(`Connecting to room ${roomId}...`);
const connection = createConnection(roomId);
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
connection.connect();
// Cleanup function - runs before next effect and on unmount
return () => {
console.log(`Disconnecting from room ${roomId}...`);
connection.disconnect();
};
}, [roomId]); // When roomId changes, cleanup runs, then effect runs again
return <div>{/* Render messages */}</div>;
}
// Execution flow when roomId changes from 'general' to 'random':
// 1. Cleanup runs: disconnect from 'general'
// 2. Effect runs: connect to 'random'
// Timer cleanup example
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// Cleanup: clear interval to prevent memory leak
return () => clearInterval(intervalId);
}, []); // Empty array means cleanup only runs on unmount
return <div>Count: {count}</div>;
}
// Event listener cleanup example
function WindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
const handleResize = () => {
setSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
// Cleanup: remove event listener
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>{size.width} x {size.height}</div>;
}
References:
↑ Back to topReact Hooks Fundamentals
What is the difference between class components and functional components with Hooks?
The 30-Second Answer:
Class components use lifecycle methods and this.state for state management, while functional components with Hooks use functions like useState and useEffect. Functional components with Hooks are more concise, avoid this binding issues, and make it easier to reuse stateful logic without changing component hierarchy.
The 2-Minute Answer (If They Want More):
Class components and functional components with Hooks represent two different programming paradigms in React. Class components follow object-oriented programming principles, using classes with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. They manage state through this.state and this.setState, and require careful management of this binding, often needing .bind(this) in the constructor or arrow function properties.
Functional components with Hooks follow a more functional programming approach. They're simpler JavaScript functions that return JSX, and they use Hooks to add state and side effects. useState replaces this.state and this.setState, while useEffect consolidates the functionality of multiple lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount) into a single API. This reduces code duplication since related logic that was split across lifecycle methods can now be grouped together.
One key difference is how they handle closures and references. In class components, this.state and this.props always refer to the latest values, which can cause issues with stale data in async operations. In functional components, each render has its own "snapshot" of props and state due to closures, which can prevent certain bugs but requires understanding how closures work with effects and callbacks.
Hooks also enable better code reuse through custom Hooks—simple functions that use other Hooks. In class components, reusing stateful logic required patterns like Higher-Order Components or render props, which added complexity and nesting. Custom Hooks let you extract and share logic without changing your component structure.
Performance-wise, both approaches can be equally optimized. Class components use shouldComponentUpdate or PureComponent, while functional components use React.memo, useMemo, and useCallback. Modern React recommends functional components with Hooks for new code, though class components remain fully supported.
Code Example:
// ==================== CLASS COMPONENT ====================
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null,
count: 0
};
// Need to bind methods
this.handleIncrement = this.handleIncrement.bind(this);
}
componentDidMount() {
// Fetch user data
this.fetchUser();
// Set up subscription
this.subscription = subscribeToUser(this.props.userId, this.handleUserUpdate);
}
componentDidUpdate(prevProps) {
// Re-fetch if userId changed
if (prevProps.userId !== this.props.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
// Clean up subscription
if (this.subscription) {
this.subscription.unsubscribe();
}
}
fetchUser = async () => {
this.setState({ loading: true });
try {
const user = await fetch(`/api/users/${this.props.userId}`).then(r => r.json());
this.setState({ user, loading: false });
} catch (error) {
this.setState({ error, loading: false });
}
};
handleUserUpdate = (user) => {
this.setState({ user });
};
handleIncrement() {
this.setState({ count: this.state.count + 1 });
}
render() {
const { user, loading, error, count } = this.state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Count: {count}</p>
<button onClick={this.handleIncrement}>Increment</button>
</div>
);
}
}
// ==================== FUNCTIONAL COMPONENT WITH HOOKS ====================
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [count, setCount] = useState(0);
// Fetch user data - combines componentDidMount and componentDidUpdate
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (!cancelled) {
setUser(userData);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err);
setLoading(false);
}
}
};
fetchUser();
// Cleanup function (like componentWillUnmount)
return () => {
cancelled = true;
};
}, [userId]); // Only re-run when userId changes
// Set up subscription - combines componentDidMount and componentWillUnmount
useEffect(() => {
const handleUserUpdate = (updatedUser) => {
setUser(updatedUser);
};
const subscription = subscribeToUser(userId, handleUserUpdate);
// Cleanup subscription
return () => {
subscription.unsubscribe();
};
}, [userId]);
// No need to bind - arrow functions work naturally
const handleIncrement = () => {
setCount(count + 1);
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
// ==================== CUSTOM HOOK FOR REUSE ====================
// With Hooks, we can extract the user fetching logic into a reusable Hook
function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
if (!cancelled) {
setUser(userData);
setLoading(false);
}
} catch (err) {
if (!cancelled) {
setError(err);
setLoading(false);
}
}
};
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
return { user, loading, error };
}
// Now UserProfile is much simpler
function UserProfileSimplified({ userId }) {
const { user, loading, error } = useUser(userId);
const [count, setCount] = useState(0);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Key Differences Summary:
graph LR
subgraph "Class Components"
A1[State: this.state]
A2[Updates: this.setState]
A3[Lifecycle Methods]
A4[this binding]
A5[HOCs/Render Props for reuse]
end
subgraph "Functional Components + Hooks"
B1[State: useState]
B2[Updates: setState function]
B3[useEffect for side effects]
B4[No this keyword]
B5[Custom Hooks for reuse]
end
A1 -.->|replaced by| B1
A2 -.->|replaced by| B2
A3 -.->|replaced by| B3
A4 -.->|eliminated| B4
A5 -.->|replaced by| B5
References:
↑ Back to topuseState Hook
What is the difference between useState with a value vs a function initializer?
The 30-Second Answer:
useState(value) evaluates the value on every render but only uses it on the first render. useState(() => value) (lazy initialization) only executes the function once during the initial render. Use lazy initialization when the initial state is expensive to compute, like reading from localStorage or performing complex calculations.
The 2-Minute Answer (If They Want More):
When you pass a value directly to useState, that expression is evaluated on every single render of the component, even though React only uses it during the initial render. This can cause performance problems if calculating the initial state is expensive - for example, filtering a large array, parsing JSON from localStorage, or performing complex computations.
Lazy initialization solves this by accepting a function instead of a value. React only calls this initializer function once, during the first render, and ignores it completely on subsequent renders. The function should be pure, take no arguments, and return the initial state value.
This optimization is particularly important when the initial state depends on expensive operations. Without lazy initialization, you'd be unnecessarily repeating expensive work on every render just to have React throw away the result. With complex applications that re-render frequently, this wasted computation can accumulate and impact performance.
Common use cases for lazy initialization include reading from Web APIs (localStorage, sessionStorage), filtering or mapping large data sets, creating initial state from props that require transformation, or any computation that involves significant processing time.
Code Example:
import { useState } from 'react';
// BAD: Expensive computation runs on EVERY render
function BadExample() {
// This JSON.parse executes every single render!
const [user, setUser] = useState(
JSON.parse(localStorage.getItem('user') || '{}')
);
return <div>{user.name}</div>;
}
// GOOD: Expensive computation runs ONLY on first render
function GoodExample() {
// This function only executes once
const [user, setUser] = useState(() => {
console.log('Initializing user - only runs once!');
const saved = localStorage.getItem('user');
return saved ? JSON.parse(saved) : {};
});
return <div>{user.name}</div>;
}
// Example with complex computation
function DataProcessor({ rawData }) {
// BAD: Processes 10,000 items on every render
const [processed, setProcessed] = useState(
rawData.map(item => expensiveTransform(item))
);
// GOOD: Processes only once
const [processedLazy, setProcessedLazy] = useState(() => {
console.log('Processing data - only once!');
return rawData.map(item => expensiveTransform(item));
});
return <div>{/* render processed data */}</div>;
}
function expensiveTransform(item) {
// Simulate expensive operation
let result = item;
for (let i = 0; i < 1000000; i++) {
result = result + i;
}
return result;
}
// Practical example: Form with default values
function UserForm({ userId }) {
const [formData, setFormData] = useState(() => {
// Only fetch and parse once on mount
const cached = localStorage.getItem(`form-${userId}`);
if (cached) {
return JSON.parse(cached);
}
// Default form structure
return {
name: '',
email: '',
preferences: {
theme: 'light',
notifications: true
}
};
});
const handleSubmit = (e) => {
e.preventDefault();
localStorage.setItem(`form-${userId}`, JSON.stringify(formData));
};
return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}
Performance Comparison:
graph TD
A[Component Renders] --> B{useState type?}
B -->|Direct Value| C[Evaluate expression]
B -->|Function Initializer| D{First render?}
C --> E[React uses value on first render only]
C --> F[Value discarded on subsequent renders]
D -->|Yes| G[Call function once]
D -->|No| H[Skip function completely]
F --> I[Wasted computation on every render]
G --> J[State initialized]
H --> J
style I fill:#ffcccc
style J fill:#ccffcc
References:
- React useState - Lazy Initialization
- Optimizing Performance in React
- Web.dev - React Performance Optimization
useContext Hook
What is useContext and how does it work?
The 30-Second Answer: useContext is a React Hook that lets you read and subscribe to context from your component without prop drilling. It accepts a Context object created by React.createContext() and returns the current context value, which is determined by the nearest Context Provider above the calling component in the tree.
The 2-Minute Answer (If They Want More): useContext solves the problem of "prop drilling" where you have to pass props through many intermediate components that don't need the data themselves. When you call useContext(MyContext), React looks up the component tree to find the nearest MyContext.Provider and returns its value prop. If the value changes, React automatically re-renders all components that consume that context.
The hook works by establishing a subscription to the context. When the Provider's value changes, React triggers a re-render of all components using that context, even if their parent components are memoized. This makes context particularly useful for data that needs to be accessible by many components at different nesting levels, such as theme data, user authentication, or locale preferences.
It's important to understand that useContext doesn't "provide" values—it only reads them. You still need to wrap your component tree with a Context.Provider component higher up in the tree to actually provide the value. The context value is always taken from the closest matching Provider above the component in the tree.
One critical performance consideration: every component that calls useContext will re-render when the context value changes, regardless of whether it uses all parts of that value. This is why it's often recommended to split contexts by their update frequency rather than grouping all related data into a single context.
Code Example:
import { createContext, useContext, useState } from 'react';
// 1. Create the context
const ThemeContext = createContext(null);
// 2. Provider component
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. Intermediate component (doesn't need theme props)
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
// 4. Consumer component using useContext
function ThemedButton() {
// Read the context value
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333'
}}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Toggle Theme (Current: {theme})
</button>
);
}
How Context Lookup Works:
graph TD
A[Component calls useContext] --> B{Find nearest Provider up the tree}
B -->|Provider found| C[Return Provider's value]
B -->|No Provider found| D[Return default value from createContext]
C --> E[Subscribe to value changes]
E --> F[Re-render when value changes]
References:
↑ Back to topuseRef Hook
What is the difference between useRef and useState?
The 30-Second Answer: useState triggers re-renders when updated and updates are asynchronous, while useRef doesn't trigger re-renders and updates are synchronous. Use useState for values that affect the UI, and useRef for values that don't need to trigger visual updates like DOM references or mutable instance variables.
The 2-Minute Answer (If They Want More):
The fundamental difference lies in how React handles changes. When you call setState, React schedules a re-render, batches the update, and eventually re-executes your component with the new state value. This process is asynchronous—you can't immediately read the new state value after calling setState. useState is designed for data that directly affects what users see.
In contrast, useRef gives you a mutable object that persists across renders. When you modify ref.current, the change happens immediately (synchronously), and React doesn't re-render the component. The ref object itself remains the same reference between renders—only its .current property changes.
This makes useState perfect for UI state (form values, toggle states, lists) while useRef excels at storing values that need to persist but shouldn't trigger renders (DOM nodes, timer IDs, previous prop values, WebSocket connections). A common pattern is using useRef to track the previous value of a state variable—the state changes trigger renders, while the ref silently tracks history.
Performance-wise, updating refs is cheaper since it skips the entire re-render process. However, this also means React won't automatically reflect ref changes in the UI—you'd need to manually manipulate the DOM or combine refs with state updates to achieve that.
Code Example:
import { useState, useRef, useEffect } from 'react';
function StateVsRefComparison() {
const [stateCount, setStateCount] = useState(0);
const refCount = useRef(0);
const renderCount = useRef(0);
// Track renders
useEffect(() => {
renderCount.current += 1;
});
const incrementState = () => {
setStateCount(prev => prev + 1);
// Can't access new value immediately
console.log('State (old):', stateCount); // Shows old value
};
const incrementRef = () => {
refCount.current += 1;
// New value available immediately
console.log('Ref (new):', refCount.current); // Shows new value
};
return (
<div>
<h3>Comparison Demo</h3>
{/* State updates trigger re-render */}
<div>
<p>State Count: {stateCount}</p>
<button onClick={incrementState}>
Increment State (causes re-render)
</button>
</div>
{/* Ref updates don't trigger re-render */}
<div>
<p>Ref Count: {refCount.current}</p>
<button onClick={incrementRef}>
Increment Ref (no re-render)
</button>
<small>
Note: UI won't update until next render!
</small>
</div>
{/* Proof of render behavior */}
<p>Component rendered {renderCount.current} times</p>
{/* Practical example: Form with validation */}
<FormWithBoth />
</div>
);
}
function FormWithBoth() {
// State for UI (causes re-render)
const [email, setEmail] = useState('');
// Ref for tracking without re-render
const validationAttempts = useRef(0);
const inputRef = useRef(null);
const validateEmail = () => {
// Increment doesn't cause re-render
validationAttempts.current += 1;
const isValid = email.includes('@');
if (!isValid) {
// Use ref to access DOM
inputRef.current?.focus();
alert(`Invalid email (attempt ${validationAttempts.current})`);
}
};
return (
<div>
<input
ref={inputRef}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
<button onClick={validateEmail}>Validate</button>
</div>
);
}
Comparison Table:
graph LR
subgraph useState
A1[Update Value] --> A2[Schedule Re-render]
A2 --> A3[Async Update]
A3 --> A4[Component Re-executes]
A4 --> A5[UI Updates]
end
subgraph useRef
B1[Update ref.current] --> B2[Immediate Change]
B2 --> B3[No Re-render]
B3 --> B4[UI Unchanged]
end
style A1 fill:#ffd700,stroke:#000
style A5 fill:#98fb98,stroke:#000
style B1 fill:#87ceeb,stroke:#000
style B4 fill:#ffb6c1,stroke:#000
| Feature | useState | useRef |
|---|---|---|
| Triggers re-render | ✅ Yes | ❌ No |
| Update timing | Asynchronous | Synchronous |
| Use in JSX | Directly | Via .current |
| Persistence | Across renders | Across renders |
| Initial value | First argument | .current property |
| Best for | UI state | Non-UI data, DOM refs |
References:
↑ Back to topWhat is the difference between useRef and createRef?
The 30-Second Answer: useRef is a Hook for function components that persists the same ref object across renders, while createRef is a class component API that creates a new ref object on every render. In function components, always use useRef—createRef would create a new reference each time, losing the persistence that makes refs useful.
The 2-Minute Answer (If They Want More):
The key difference is lifecycle and intended use case. createRef was designed for class components where the ref is created once in the constructor and stored as an instance variable. Each time the class instance method runs, it accesses the same ref object from this.myRef. The ref persists because the class instance persists.
In function components, the entire component function re-executes on every render. If you called createRef() directly in the function body, you'd get a brand new ref object every single render, making it useless for its primary purposes (DOM access and value persistence). useRef solves this by leveraging React's Hook system—it returns the same ref object on every render, storing it in React's internal fiber node.
Under the hood, createRef is just { current: null }—a simple object factory. useRef is more sophisticated: on first render it creates the ref object, stores it in the component's Hook state, and returns that same object on subsequent renders. This persistence is what makes refs work in functional components.
There's rarely a legitimate use case for createRef in function components. The only scenario might be intentionally creating a fresh ref inside an event handler or callback, but even then, a plain object { current: value } would be clearer. For 99% of use cases in modern React (function components with Hooks), useRef is the correct choice.
Class components use createRef and store it as this.myRef. Function components use useRef and rely on React's Hook system for persistence. If you're writing function components (which you should be for new code), always reach for useRef.
Code Example:
import { useRef, createRef, useState, useEffect } from 'react';
// ❌ WRONG: createRef in function component
function WrongRefUsage() {
// Creates NEW ref object every render - loses reference!
const inputRef = createRef();
const [count, setCount] = useState(0);
useEffect(() => {
console.log('inputRef:', inputRef.current);
// Will be null on every render after first!
});
return (
<div>
<input ref={inputRef} defaultValue="test" />
<button onClick={() => setCount(c => c + 1)}>
Re-render ({count})
</button>
<p>Check console - ref is lost on re-render!</p>
</div>
);
}
// âś… CORRECT: useRef in function component
function CorrectRefUsage() {
// Same ref object on every render - persists!
const inputRef = useRef(null);
const [count, setCount] = useState(0);
useEffect(() => {
console.log('inputRef:', inputRef.current);
// Will have input element on every render
});
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} defaultValue="test" />
<button onClick={focusInput}>Focus Input</button>
<button onClick={() => setCount(c => c + 1)}>
Re-render ({count})
</button>
<p>Ref persists correctly!</p>
</div>
);
}
// Class component example (legacy)
class ClassComponentWithRef extends React.Component {
// âś… CORRECT: createRef in class component
constructor(props) {
super(props);
// Created once, stored as instance variable
this.inputRef = createRef();
this.state = { count: 0 };
}
focusInput = () => {
this.inputRef.current?.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} defaultValue="test" />
<button onClick={this.focusInput}>
Focus Input
</button>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Re-render ({this.state.count})
</button>
</div>
);
}
}
// Demonstrating the difference
function ComparisonDemo() {
const [renderCount, setRenderCount] = useState(0);
// useRef - same object every render
const useRefExample = useRef({ value: 'initial' });
// createRef - NEW object every render
const createRefExample = createRef();
createRefExample.current = { value: 'initial' };
// Track object identity
const useRefIdentity = useRef(null);
const createRefIdentity = useRef(null);
useEffect(() => {
// First render
if (renderCount === 0) {
useRefIdentity.current = useRefExample;
createRefIdentity.current = createRefExample;
console.log('First render - storing identities');
} else {
// Subsequent renders
console.log(
'useRef same object?',
useRefIdentity.current === useRefExample
); // true
console.log(
'createRef same object?',
createRefIdentity.current === createRefExample
); // false
}
});
return (
<div>
<h3>useRef vs createRef Comparison</h3>
<button onClick={() => setRenderCount(c => c + 1)}>
Trigger Re-render ({renderCount})
</button>
<p>Check console to see object identity</p>
<CorrectRefUsage />
<WrongRefUsage />
</div>
);
}
Visual Comparison:
sequenceDiagram
participant FC as Function Component
participant React
participant Ref
Note over FC,Ref: useRef Behavior
FC->>React: useRef(null) - First Render
React->>Ref: Create { current: null }
React-->>FC: Return ref object
FC->>React: useRef(null) - Second Render
React->>Ref: Retrieve same object
React-->>FC: Return SAME ref object
Note over FC,Ref: createRef Behavior
FC->>Ref: createRef() - First Render
Ref-->>FC: Return NEW { current: null }
FC->>Ref: createRef() - Second Render
Ref-->>FC: Return NEW { current: null }
Note over FC: Previous ref lost!
When to Use Each:
| Scenario | Use | Reason |
|---|---|---|
| Function component | useRef | Persists across renders |
| Class component | createRef | Stored as instance variable |
| Hook/custom hook | useRef | Hook context required |
| One-time ref creation | Either | If truly one-time, doesn't matter |
| Dynamic ref in callback | Plain object | { current: value } is clearer |
Implementation Difference:
// Simplified implementation
// createRef - just creates an object
function createRef() {
return { current: null };
}
// useRef - uses React's Hook system for persistence
function useRef(initialValue) {
// React's internal Hook state stores the ref
const [ref] = useState(() => ({ current: initialValue }));
return ref;
}
// In reality, useRef is more like:
function useRef(initialValue) {
// Get the Hook slot from fiber's memoizedState
const hook = getOrCreateHook();
if (hook.memoizedState === null) {
// First render: create ref
hook.memoizedState = { current: initialValue };
}
// Subsequent renders: return same ref
return hook.memoizedState;
}
Migration Guide:
// Class component (old way)
class OldComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = createRef(); // âś… Correct for classes
}
componentDidMount() {
this.myRef.current.focus();
}
render() {
return <input ref={this.myRef} />;
}
}
// Function component (modern way)
function NewComponent() {
const myRef = useRef(null); // âś… Correct for functions
useEffect(() => {
myRef.current?.focus();
}, []);
return <input ref={myRef} />;
}
References:
- React Docs - useRef
- React Docs - createRef
- React Hooks FAQ - How do I create expensive objects lazily?
useMemo and useCallback
What is the difference between useMemo and useCallback?
The 30-Second Answer:
useMemo memoizes the result of a function (a computed value), while useCallback memoizes the function itself. In fact, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) - one returns a value, the other returns a function.
The 2-Minute Answer (If They Want More):
The fundamental difference lies in what they return. useMemo runs your function and returns its result, storing that result for reuse. useCallback doesn't run your function - it returns the function itself, preserving its reference identity across renders.
Think of it this way:
useMemo(() => computeExpensiveValue(a, b), [a, b])runs the computation and returns the valueuseCallback((x) => doSomething(x), [dep])returns the function(x) => doSomething(x)
Both hooks serve the optimization goal of preventing unnecessary work, but they target different scenarios. Use useMemo when you want to avoid expensive recalculations - the work happens inside the hook, and you use the result. Use useCallback when you want to prevent creating new function instances - the work happens later when the function is called, but you need the same function reference.
A practical way to remember: if you're computing a value (number, string, object, array), use useMemo. If you're defining a function to be called later (event handler, callback), use useCallback.
Code Example:
import { useMemo, useCallback, useState } from 'react';
function ComparisonExample({ items, threshold }) {
const [filter, setFilter] = useState('');
// useMemo: Returns the RESULT (an array)
const expensiveCalculation = useMemo(() => {
console.log('Computing filtered items...');
return items
.filter(item => item.value > threshold)
.map(item => ({ ...item, processed: true }));
}, [items, threshold]);
// expensiveCalculation is an ARRAY
// useCallback: Returns the FUNCTION itself
const handleFilter = useCallback((searchTerm) => {
console.log('Filter function called with:', searchTerm);
setFilter(searchTerm);
}, []);
// handleFilter is a FUNCTION
// They're actually related - useCallback is syntactic sugar:
// useCallback(fn, deps) === useMemo(() => fn, deps)
// This useCallback:
const myCallback = useCallback(() => {
console.log('Hello');
}, []);
// Is equivalent to this useMemo:
const myCallbackEquivalent = useMemo(() => {
return () => {
console.log('Hello');
};
}, []);
return (
<div>
{/* Using the memoized VALUE */}
<p>Expensive items count: {expensiveCalculation.length}</p>
{/* Using the memoized FUNCTION */}
<button onClick={() => handleFilter('test')}>Filter</button>
</div>
);
}
// Visual comparison
function VisualComparison() {
const [count, setCount] = useState(0);
// useMemo - runs factorial calculation, returns NUMBER
const factorial = useMemo(() => {
const calculate = (n) => (n <= 1 ? 1 : n * calculate(n - 1));
return calculate(count); // Returns the RESULT
}, [count]);
// useCallback - doesn't run anything, returns FUNCTION
const increment = useCallback(() => {
setCount(c => c + 1); // This runs later when called
}, []);
return (
<div>
<p>Count: {count}</p>
<p>Factorial: {factorial}</p> {/* Using the computed VALUE */}
<button onClick={increment}>Increment</button> {/* Using the FUNCTION */}
</div>
);
}
Mermaid Diagram:
graph TD
A[Hook Choice] --> B{What do you need?}
B -->|Computed Value| C[useMemo]
B -->|Function Reference| D[useCallback]
C --> E[useMemo runs function<br/>Returns result<br/>Example: filtered array]
D --> F[useCallback preserves function<br/>Returns function itself<br/>Example: event handler]
E --> G[Use case: Expensive<br/>calculations, derived state]
F --> H[Use case: Callbacks to<br/>memoized children, hook deps]
style C fill:#e1f5e1
style D fill:#e1e5f5
References:
↑ Back to topuseReducer Hook
What is the difference between useState and useReducer?
The 30-Second Answer: useState manages simple state with direct updates via setState, while useReducer manages complex state through actions dispatched to a reducer function. useState is ideal for independent state values; useReducer excels when state transitions follow specific rules, multiple values change together, or updates depend on previous state in complex ways.
The 2-Minute Answer (If They Want More): The fundamental difference lies in how state updates are handled. useState provides direct state mutation through a setter function, while useReducer centralizes update logic in a reducer, separating what happened (actions) from how state changes (reducer logic).
Key differences:
Complexity: useState for simple state (booleans, strings, numbers, simple objects). useReducer for complex state with multiple related fields or intricate update logic.
Update patterns: useState updates are imperative ("set state to this value"). useReducer updates are declarative ("this event happened, you figure out the new state").
Performance: dispatch from useReducer is stable across renders, making it better for optimizing child components. useState setters are also stable, but useReducer's centralized logic can prevent redundant calculations.
Testability: Reducers are pure functions, easily testable in isolation. useState logic is mixed with component code, harder to test.
State dependencies: useReducer handles complex state dependencies better since all update logic is in one place, reducing bugs from forgetting to update related state.
Learning curve: useState is simpler and more intuitive. useReducer requires understanding the reducer pattern but provides better scalability.
In practice, start with useState and refactor to useReducer when complexity grows. You can even use both in the same component for different pieces of state.
Code Example:
import { useState, useReducer } from 'react';
// EXAMPLE 1: Simple state - useState is perfect
function ToggleWithState() {
const [isOpen, setIsOpen] = useState(false);
return (
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close' : 'Open'}
</button>
);
}
// Using useReducer here would be overkill
function ToggleWithReducer() {
const [isOpen, dispatch] = useReducer(
(state, action) => action.type === 'TOGGLE' ? !state : state,
false
);
return (
<button onClick={() => dispatch({ type: 'TOGGLE' })}>
{isOpen ? 'Close' : 'Open'}
</button>
);
}
// EXAMPLE 2: Complex state - useReducer shines
// useState version - difficult to maintain, easy to make mistakes
function FormWithState() {
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitCount, setSubmitCount] = useState(0);
const [touched, setTouched] = useState({});
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setSubmitCount(submitCount + 1);
// Validate
const newErrors = {};
if (!username) newErrors.username = 'Required';
if (!email.includes('@')) newErrors.email = 'Invalid email';
if (password.length < 8) newErrors.password = 'Too short';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
// Submit logic
await submitForm({ username, email, password });
// Reset form
setUsername('');
setEmail('');
setPassword('');
setErrors({});
setTouched({});
}
setIsSubmitting(false);
};
// Many setState calls, hard to keep synchronized
const handleReset = () => {
setUsername('');
setEmail('');
setPassword('');
setErrors({});
setIsSubmitting(false);
setSubmitCount(0);
setTouched({});
};
return (
<form onSubmit={handleSubmit}>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
onBlur={() => setTouched({ ...touched, username: true })}
/>
{touched.username && errors.username && <span>{errors.username}</span>}
{/* More fields... */}
</form>
);
}
// useReducer version - centralized, predictable, easier to maintain
const initialFormState = {
values: { username: '', email: '', password: '' },
errors: {},
touched: {},
isSubmitting: false,
submitCount: 0
};
function formReducer(state, action) {
switch (action.type) {
case 'FIELD_CHANGE':
return {
...state,
values: {
...state.values,
[action.field]: action.value
},
// Clear error when user starts typing
errors: {
...state.errors,
[action.field]: undefined
}
};
case 'FIELD_BLUR':
return {
...state,
touched: {
...state.touched,
[action.field]: true
}
};
case 'SUBMIT_START':
return {
...state,
isSubmitting: true,
submitCount: state.submitCount + 1
};
case 'SUBMIT_SUCCESS':
return initialFormState; // Clean reset
case 'SUBMIT_ERROR':
return {
...state,
isSubmitting: false,
errors: action.errors
};
case 'RESET':
return initialFormState;
default:
return state;
}
}
function FormWithReducer() {
const [state, dispatch] = useReducer(formReducer, initialFormState);
const handleChange = (field) => (e) => {
dispatch({
type: 'FIELD_CHANGE',
field,
value: e.target.value
});
};
const handleBlur = (field) => () => {
dispatch({ type: 'FIELD_BLUR', field });
};
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: 'SUBMIT_START' });
// Validate
const errors = validateForm(state.values);
if (Object.keys(errors).length > 0) {
dispatch({ type: 'SUBMIT_ERROR', errors });
return;
}
try {
await submitForm(state.values);
dispatch({ type: 'SUBMIT_SUCCESS' });
} catch (error) {
dispatch({
type: 'SUBMIT_ERROR',
errors: { submit: error.message }
});
}
};
return (
<form onSubmit={handleSubmit}>
<input
value={state.values.username}
onChange={handleChange('username')}
onBlur={handleBlur('username')}
/>
{state.touched.username && state.errors.username && (
<span>{state.errors.username}</span>
)}
{/* More fields... */}
<button
type="submit"
disabled={state.isSubmitting}
>
Submit
</button>
<button
type="button"
onClick={() => dispatch({ type: 'RESET' })}
>
Reset
</button>
</form>
);
}
function validateForm(values) {
const errors = {};
if (!values.username) errors.username = 'Required';
if (!values.email.includes('@')) errors.email = 'Invalid email';
if (values.password.length < 8) errors.password = 'Too short';
return errors;
}
async function submitForm(values) {
// API call
}
// EXAMPLE 3: When to use both together
function TodoApp() {
// Simple UI state - useState
const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
const [searchQuery, setSearchQuery] = useState('');
// Complex data state - useReducer
const [state, dispatch] = useReducer(todoReducer, initialTodoState);
const visibleTodos = state.todos
.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true;
})
.filter(todo =>
todo.text.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div>
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search..."
/>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{visibleTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
dispatch={dispatch}
/>
))}
</div>
);
}
const initialTodoState = {
todos: [],
lastId: 0
};
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
todos: [...state.todos, {
id: state.lastId + 1,
text: action.text,
completed: false
}],
lastId: state.lastId + 1
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id)
};
default:
return state;
}
}
Decision Tree:
graph TD
A[Need to manage state] --> B{Simple value?}
B -->|Yes| C{Single independent value?}
B -->|No| D{Multiple related values?}
C -->|Yes| E[Use useState]
C -->|No| F{Complex dependencies?}
D -->|Yes| G{Updates happen together?}
D -->|No| H[Multiple useState]
F -->|Yes| I[Use useReducer]
F -->|No| E
G -->|Yes| I
G -->|No| H
I --> J{Need global state?}
J -->|Yes| K[useReducer + useContext]
J -->|No| L[useReducer alone]
References:
↑ Back to topPerformance and Best Practices
What is the React Hooks ESLint plugin and why is it important?
The 30-Second Answer:
The eslint-plugin-react-hooks plugin enforces the Rules of Hooks (call Hooks at top level only) and validates dependency arrays in useEffect/useCallback/useMemo. It catches bugs before runtime by ensuring you include all dependencies and avoid conditional Hook calls, preventing subtle issues with stale closures and broken renders.
The 2-Minute Answer (If They Want More): The ESLint plugin for React Hooks is essential tooling that enforces two critical rules. First, it ensures Hooks are only called at the top level of components or custom hooks, never inside conditions, loops, or nested functions. This guarantees React can track Hook state correctly between renders. Second, it validates that all dependencies are included in dependency arrays for useEffect, useCallback, useMemo, and useImperativeHandle.
The dependency validation is particularly valuable because it catches stale closure bugs that are otherwise difficult to spot. When you reference a variable inside a Hook callback but forget to include it in dependencies, you create a closure that captures the old value. The plugin identifies these issues immediately, suggesting which dependencies to add. Its suggestions are almost always correct - if you think you need to disable the rule, you likely need to refactor your code instead.
Installing the plugin is straightforward and works with Create React App by default. For custom setups, add eslint-plugin-react-hooks to your project and configure the two rules: rules-of-hooks (error) and exhaustive-deps (warn). The exhaustive-deps rule can be set to "error" for stricter enforcement, though "warn" is standard since there are occasional legitimate suppressions.
The plugin dramatically improves code quality and reduces debugging time. It catches bugs during development that would otherwise only surface in production under specific conditions. It also educates developers about Hook best practices through its warnings, helping teams write better React code. While you can suppress specific warnings with ESLint comments, doing so should be rare and well-documented - the plugin's suggestions are correct 99% of the time.
Code Example:
// âś… Installation and Configuration
// 1. Install the plugin
// npm install eslint-plugin-react-hooks --save-dev
// 2. Configure in .eslintrc.js or .eslintrc.json
{
"extends": [
"react-app", // Create React App includes this by default
"plugin:react-hooks/recommended" // Or configure manually below
],
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error", // Enforces Rules of Hooks
"react-hooks/exhaustive-deps": "warn" // Validates dependency arrays
}
}
// ❌ RULE 1: rules-of-hooks violations
function BadComponent({ condition }) {
// ❌ Error: Hooks can't be called conditionally
if (condition) {
const [value, setValue] = useState(0); // ESLint error!
}
// ❌ Error: Hooks can't be called in loops
for (let i = 0; i < 10; i++) {
useEffect(() => { // ESLint error!
console.log(i);
});
}
// ❌ Error: Hooks can't be called in nested functions
const handleClick = () => {
const [count, setCount] = useState(0); // ESLint error!
};
return <div>Bad Component</div>;
}
// âś… CORRECT: Proper Hook usage
function GoodComponent({ condition }) {
// âś… Always call Hooks at top level
const [value, setValue] = useState(0);
// âś… Use conditional logic INSIDE the Hook
useEffect(() => {
if (condition) {
console.log('Condition is true');
}
}, [condition]);
return <div>Good Component</div>;
}
// ❌ RULE 2: exhaustive-deps violations
function DependencyIssues({ userId, filter }) {
const [data, setData] = useState([]);
const [count, setCount] = useState(0);
// ❌ Warning: 'userId' is used but not in dependencies
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setData);
}, []); // ESLint warning: missing dependency 'userId'
// ❌ Warning: 'filter' and 'data' missing from dependencies
const filteredData = useMemo(() => {
return data.filter(item => item.name.includes(filter));
}, []); // ESLint warning: missing dependencies 'data' and 'filter'
// ❌ Warning: 'count' missing from dependencies
const handleClick = useCallback(() => {
console.log('Current count:', count);
}, []); // ESLint warning: missing dependency 'count'
return <div>Data: {data.length}</div>;
}
// âś… CORRECT: All dependencies included
function CorrectDependencies({ userId, filter }) {
const [data, setData] = useState([]);
const [count, setCount] = useState(0);
// âś… Include all external values used in effect
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(result => {
if (!cancelled) {
setData(result);
}
});
return () => {
cancelled = true;
};
}, [userId]); // âś… userId included
// âś… Include all values used in computation
const filteredData = useMemo(() => {
return data.filter(item => item.name.includes(filter));
}, [data, filter]); // âś… Both dependencies included
// âś… Include all captured values
const handleClick = useCallback(() => {
console.log('Current count:', count);
}, [count]); // âś… count included
return <div>Data: {filteredData.length}</div>;
}
// âś… LEGITIMATE SUPPRESSIONS (rare cases)
function LegitimateSuppressions() {
const [count, setCount] = useState(0);
// Scenario 1: Want effect to run only on mount
useEffect(() => {
// Initialize something that should only happen once
console.log('Component mounted');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Explicitly want empty deps despite using external values
// Scenario 2: Using function from props that changes reference but not behavior
function ComponentWithCallback({ onEvent }) {
useEffect(() => {
// onEvent changes every render but we only want to subscribe once
const unsubscribe = subscribe(onEvent);
return unsubscribe;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Suppressed with good reason (documented)
}
// ⚠️ BETTER: Use useEvent pattern instead (when available)
function BetterApproach({ onEvent }) {
const onEventRef = useRef(onEvent);
useEffect(() => {
onEventRef.current = onEvent;
});
useEffect(() => {
const unsubscribe = subscribe(() => {
onEventRef.current(); // Always calls latest version
});
return unsubscribe;
}, []); // No suppression needed, no dependencies
}
}
// âś… CUSTOM HOOKS: Plugin validates these too
function useCustomHook(value) {
const [state, setState] = useState(value);
// ❌ ESLint will warn about missing 'value' dependency
useEffect(() => {
setState(value);
}, []); // Warning!
// âś… Correct: include dependency
useEffect(() => {
setState(value);
}, [value]);
return state;
}
// âś… COMPLEX SCENARIO: Proper refactoring when plugin warns
function ComplexComponent({ userId, filters, sortBy }) {
const [data, setData] = useState([]);
// Original: ESLint warns about all dependencies
// useEffect(() => {
// const params = buildQueryParams(filters, sortBy);
// fetchData(userId, params).then(setData);
// }, []); // Many missing dependencies!
// âś… Solution 1: Include all dependencies
useEffect(() => {
const params = buildQueryParams(filters, sortBy);
fetchData(userId, params).then(setData);
}, [userId, filters, sortBy]); // All dependencies included
// âś… Solution 2: Memoize complex objects
const params = useMemo(
() => buildQueryParams(filters, sortBy),
[filters, sortBy]
);
useEffect(() => {
fetchData(userId, params).then(setData);
}, [userId, params]); // Stable reference to params
// âś… Solution 3: Extract to custom hook
function useFetchData(userId, filters, sortBy) {
const [data, setData] = useState([]);
useEffect(() => {
const params = buildQueryParams(filters, sortBy);
fetchData(userId, params).then(setData);
}, [userId, filters, sortBy]);
return data;
}
// const data = useFetchData(userId, filters, sortBy);
return <div>Data: {data.length}</div>;
}
// Helper functions
function buildQueryParams(filters, sortBy) {
return { filters, sortBy };
}
async function fetchData(userId, params) {
return [];
}
function subscribe(callback) {
return () => {};
}
ESLint Plugin Rules Flow:
flowchart TD
A[Write Hook Code] --> B{ESLint Analysis}
B --> C[Check: rules-of-hooks]
C --> C1{Hook at top level?}
C1 -->|No| C2[❌ ERROR: Conditional/Loop/Nested Hook]
C1 -->|Yes| C3[âś… Pass]
B --> D[Check: exhaustive-deps]
D --> D1{Has dependency array?}
D1 -->|No array| D2[Runs every render]
D2 --> D3[⚠️ Usually wrong, add array]
D1 -->|Empty array []| D4{Uses external values?}
D4 -->|Yes| D5[⚠️ WARNING: Missing dependencies]
D4 -->|No| D6[âś… Pass - runs once]
D1 -->|Has dependencies| D7{All values included?}
D7 -->|No| D8[⚠️ WARNING: Missing dependencies]
D7 -->|Yes| D9{Unnecessary deps?}
D9 -->|Yes| D10[⚠️ WARNING: Remove unnecessary deps]
D9 -->|No| D11[âś… Pass]
D5 --> E[Fix Issues]
D8 --> E
D10 --> E
C2 --> F[Refactor Code]
E --> G[Re-run ESLint]
F --> G
G --> H{All checks pass?}
H -->|No| B
H -->|Yes| I[âś… Safe to Deploy]
Common ESLint Warnings and Solutions:
## Common ESLint Plugin Warnings
### Warning: React Hook useEffect has a missing dependency
**Problem:** You're using a variable inside useEffect but not listing it in dependencies.
**Solution:**
```js
// ❌ Before
useEffect(() => {
console.log(userId);
}, []);
// âś… After
useEffect(() => {
console.log(userId);
}, [userId]);
Warning: React Hook useEffect has an unnecessary dependency
Problem: You listed a dependency that isn't used in the effect.
Solution:
// ❌ Before
useEffect(() => {
console.log('mounted');
}, [userId, count]); // count not used
// âś… After
useEffect(() => {
console.log('mounted');
}, [userId]);
Warning: The ref value will likely have changed by the time this effect cleanup function runs
Problem: Using ref.current in cleanup function.
Solution:
// ❌ Before
useEffect(() => {
const id = ref.current;
return () => cleanup(ref.current); // Might be different!
}, []);
// âś… After
useEffect(() => {
const id = ref.current;
return () => cleanup(id); // Captures current value
}, []);
Error: React Hook is called conditionally
Problem: Calling Hook inside if/for/nested function.
Solution:
// ❌ Before
if (condition) {
useEffect(() => {});
}
// âś… After
useEffect(() => {
if (condition) {
// conditional logic inside
}
}, [condition]);
**References:**
- [ESLint Plugin React Hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)
- [React Docs - Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks)
- [ESLint Rule: exhaustive-deps](https://github.com/facebook/react/issues/14920)
- [When to disable exhaustive-deps](https://kentcdodds.com/blog/how-to-optimize-your-context-value)
↑ Back to top
Advanced Hooks
What is useTransition and how does it help with performance?
The 30-Second Answer:
useTransition marks state updates as non-urgent "transitions," allowing React to keep the UI responsive by interrupting slow renders if more urgent updates arrive. It returns a isPending flag to show loading states while transitions complete, making it ideal for navigation and filtering without blocking user interactions.
The 2-Minute Answer (If They Want More):
useTransition is a concurrent React feature that helps you maintain a responsive UI even when performing expensive state updates. It returns an array with an isPending boolean and a startTransition function. Wrap state updates in startTransition to tell React they're lower priority and can be interrupted if more urgent updates (like typing in an input) occur.
The key benefit is preventing UI freezes. Without transitions, updating state that triggers expensive re-renders blocks all interactions until complete. With transitions, React can pause the expensive work to handle urgent updates, then resume when the main thread is free. The isPending flag lets you show loading indicators during transitions.
Common use cases include: tab switching with heavy content, filtering/sorting large lists, search results, route navigation, form submissions with complex validation, and any scenario where user input should remain responsive while background processing happens. It's particularly valuable for maintaining 60fps interactions even when components take hundreds of milliseconds to render.
Unlike useDeferredValue which defers a value, useTransition lets you control when to trigger the transition. Use useTransition when you control the state update (like clicking a button), and useDeferredValue when you're responding to a prop or state you don't control. Both can be combined: useTransition for the update that changes the value, and useDeferredValue in child components that consume it.
Code Example:
import { useState, useTransition, memo } from 'react';
// Example 1: Tab switching with heavy content
function TabContainer() {
const [activeTab, setActiveTab] = useState('home');
const [isPending, startTransition] = useTransition();
const handleTabClick = (tab) => {
// Mark this state update as a transition
startTransition(() => {
setActiveTab(tab);
});
};
return (
<div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<button
onClick={() => handleTabClick('home')}
disabled={isPending}
style={{
background: activeTab === 'home' ? '#007bff' : '#f0f0f0',
color: activeTab === 'home' ? 'white' : 'black'
}}
>
Home {activeTab === 'home' && isPending && '(loading...)'}
</button>
<button
onClick={() => handleTabClick('posts')}
disabled={isPending}
style={{
background: activeTab === 'posts' ? '#007bff' : '#f0f0f0',
color: activeTab === 'posts' ? 'white' : 'black'
}}
>
Posts {activeTab === 'posts' && isPending && '(loading...)'}
</button>
<button
onClick={() => handleTabClick('contact')}
disabled={isPending}
style={{
background: activeTab === 'contact' ? '#007bff' : '#f0f0f0',
color: activeTab === 'contact' ? 'white' : 'black'
}}
>
Contact {activeTab === 'contact' && isPending && '(loading...)'}
</button>
</div>
{/* Show current tab with loading indicator */}
<div style={{ opacity: isPending ? 0.5 : 1 }}>
{activeTab === 'home' && <HomeTab />}
{activeTab === 'posts' && <PostsTab />}
{activeTab === 'contact' && <ContactTab />}
</div>
</div>
);
}
// Expensive tab components
const PostsTab = memo(() => {
// Simulate expensive rendering
const posts = Array.from({ length: 5000 }, (_, i) => ({
id: i,
title: `Post ${i}`,
content: `Content for post ${i}`
}));
return (
<div>
<h2>Posts</h2>
{posts.slice(0, 100).map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</article>
))}
</div>
);
});
// Example 2: Search/filter with transitions
function ProductList() {
const [query, setQuery] = useState('');
const [filterValue, setFilterValue] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// Update input immediately (no transition)
setQuery(value);
// Defer the expensive filter operation
startTransition(() => {
setFilterValue(value);
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="Search products..."
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '2px solid #ccc'
}}
/>
{isPending && (
<div style={{ padding: '8px', background: '#fff3cd' }}>
Updating results...
</div>
)}
<ExpensiveProductGrid filter={filterValue} />
</div>
);
}
const ExpensiveProductGrid = memo(({ filter }) => {
const products = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
category: ['Electronics', 'Clothing', 'Books'][i % 3]
}));
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase()) ||
p.category.toLowerCase().includes(filter.toLowerCase())
);
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
{filtered.slice(0, 100).map(product => (
<div key={product.id} style={{ border: '1px solid #ddd', padding: '16px' }}>
<h3>{product.name}</h3>
<p>{product.category}</p>
</div>
))}
</div>
);
});
// Example 3: Form submission with validation
function ComplexForm() {
const [formData, setFormData] = useState({ name: '', email: '' });
const [errors, setErrors] = useState({});
const [isPending, startTransition] = useTransition();
const handleSubmit = (e) => {
e.preventDefault();
// Run expensive validation in a transition
startTransition(() => {
const newErrors = validateForm(formData); // Expensive validation
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
submitForm(formData); // Expensive submission
}
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Name"
/>
{errors.name && <p style={{ color: 'red' }}>{errors.name}</p>}
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
placeholder="Email"
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
// Example 4: Comparison - Without vs With useTransition
function WithoutTransition() {
const [tab, setTab] = useState('tab1');
return (
<div>
{/* Clicking feels laggy - UI freezes during expensive render */}
<button onClick={() => setTab('tab1')}>Tab 1</button>
<button onClick={() => setTab('tab2')}>Tab 2</button>
<ExpensiveComponent tab={tab} />
</div>
);
}
function WithTransition() {
const [tab, setTab] = useState('tab1');
const [isPending, startTransition] = useTransition();
return (
<div>
{/* Clicking feels instant - React can interrupt expensive render */}
<button
onClick={() => startTransition(() => setTab('tab1'))}
style={{ opacity: isPending && tab === 'tab1' ? 0.5 : 1 }}
>
Tab 1 {isPending && tab === 'tab1' && '⏳'}
</button>
<button
onClick={() => startTransition(() => setTab('tab2'))}
style={{ opacity: isPending && tab === 'tab2' ? 0.5 : 1 }}
>
Tab 2 {isPending && tab === 'tab2' && '⏳'}
</button>
<ExpensiveComponent tab={tab} />
</div>
);
}
// Example 5: Route navigation
function Router() {
const [route, setRoute] = useState('/');
const [isPending, startTransition] = useTransition();
const navigate = (path) => {
startTransition(() => {
setRoute(path);
});
};
return (
<div>
<nav style={{ display: 'flex', gap: '16px', marginBottom: '16px' }}>
<button onClick={() => navigate('/')}>Home</button>
<button onClick={() => navigate('/about')}>About</button>
<button onClick={() => navigate('/dashboard')}>Dashboard</button>
</nav>
{isPending && (
<div style={{ padding: '8px', background: '#e3f2fd' }}>
Loading page...
</div>
)}
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{route === '/' && <HomePage />}
{route === '/about' && <AboutPage />}
{route === '/dashboard' && <DashboardPage />}
</div>
</div>
);
}
// Helper functions
function validateForm(data) {
// Simulate expensive validation
const errors = {};
if (data.name.length < 3) errors.name = 'Name too short';
if (!data.email.includes('@')) errors.email = 'Invalid email';
return errors;
}
function submitForm(data) {
console.log('Submitting:', data);
}
Transition Priority Flow:
graph TD
A[User Action] --> B{Is it wrapped in startTransition?}
B -->|No| C[Urgent Update]
B -->|Yes| D[Non-Urgent Transition]
C --> E[React renders immediately]
C --> F[Blocks UI until complete]
D --> G[React renders in background]
D --> H[Can be interrupted]
D --> I[isPending = true]
H --> J[New urgent update arrives]
J --> K[Pause transition]
K --> L[Handle urgent update]
L --> M[Resume transition]
style C fill:#ff6b6b
style D fill:#51cf66
References:
↑ Back to topWhat is useImperativeHandle and when would you use it?
The 30-Second Answer:
useImperativeHandle customizes the instance value exposed to parent components when using ref. It's used with forwardRef to control what methods or properties a parent can access on a child component, maintaining encapsulation while allowing limited imperative access.
The 2-Minute Answer (If They Want More):
In React's declarative paradigm, parents typically control children through props. However, sometimes you need imperative control—like focusing an input, triggering an animation, or scrolling to a position. useImperativeHandle lets you define exactly what the parent can do through a ref, rather than exposing the entire DOM element or component instance.
This hook is particularly useful for building reusable UI libraries or complex form components where you want to expose specific methods (like focus(), clear(), validate()) without leaking implementation details. It takes three arguments: the ref to customize, a function that returns an object of methods/values to expose, and a dependency array.
Common use cases include: creating custom input components with focus/blur/clear methods, video player components with play/pause/seek controls, modal components with open/close methods, or form field components with validation methods. By using useImperativeHandle, you create a clear API contract between parent and child while keeping internal implementation details private.
The hook should be used sparingly—most React patterns favor props and state. Only use it when imperative access is genuinely necessary, such as managing focus, text selection, animations, or integrating with third-party imperative libraries.
Code Example:
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
// Custom input component with imperative methods
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
const [value, setValue] = useState('');
// Expose only specific methods to parent
useImperativeHandle(ref, () => ({
// Custom focus method with additional logic
focus: () => {
inputRef.current?.focus();
},
// Clear method
clear: () => {
setValue('');
inputRef.current?.focus();
},
// Validate method
validate: () => {
const isValid = value.length >= 3;
if (!isValid) {
inputRef.current?.setCustomValidity('Minimum 3 characters');
}
return isValid;
},
// Expose value getter (no direct state access)
getValue: () => value,
// Method to highlight text
selectAll: () => {
inputRef.current?.select();
}
}), [value]); // Recreate when value changes
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
style={{
padding: '8px',
border: '2px solid #ccc',
borderRadius: '4px'
}}
{...props}
/>
);
});
// Parent component using the custom input
function Form() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
// Use exposed imperative methods
if (!inputRef.current.validate()) {
alert('Validation failed!');
inputRef.current.focus();
return;
}
const value = inputRef.current.getValue();
console.log('Submitted:', value);
inputRef.current.clear();
};
const handleFocus = () => {
inputRef.current?.focus();
};
const handleSelectAll = () => {
inputRef.current?.selectAll();
};
return (
<form onSubmit={handleSubmit}>
<FancyInput ref={inputRef} placeholder="Enter at least 3 characters" />
<button type="submit">Submit</button>
<button type="button" onClick={handleFocus}>Focus</button>
<button type="button" onClick={handleSelectAll}>Select All</button>
</form>
);
}
// Example: Video player with imperative controls
const VideoPlayer = forwardRef(({ src }, ref) => {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current?.play(),
pause: () => videoRef.current?.pause(),
seek: (time) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
getCurrentTime: () => videoRef.current?.currentTime || 0,
getDuration: () => videoRef.current?.duration || 0
}), []);
return <video ref={videoRef} src={src} />;
});
// Usage
function VideoApp() {
const playerRef = useRef(null);
return (
<div>
<VideoPlayer ref={playerRef} src="video.mp4" />
<button onClick={() => playerRef.current?.play()}>Play</button>
<button onClick={() => playerRef.current?.pause()}>Pause</button>
<button onClick={() => playerRef.current?.seek(0)}>Restart</button>
</div>
);
}
References:
↑ Back to topCustom Hooks
What is a custom Hook and why would you create one?
The 30-Second Answer: A custom Hook is a JavaScript function that starts with "use" and can call other Hooks. You create custom Hooks to extract and reuse stateful logic between multiple components without changing component hierarchy, avoiding the problems of wrapper hell and prop drilling.
The 2-Minute Answer (If They Want More): Custom Hooks are a mechanism to reuse stateful logic across components. Unlike components, custom Hooks don't render UI - they only encapsulate logic. This solves the age-old problem of duplicated logic in class components, which previously required render props or higher-order components.
The key benefit is composability. You can build custom Hooks that use other custom Hooks, creating a chain of reusable logic. For example, you might have a useFetch Hook that uses useState and useEffect internally, or a useAuth Hook that combines useContext and useLocalStorage.
Custom Hooks also improve testability since you can test the logic independently from the components. They make your components cleaner and more focused on rendering, while the complex stateful logic lives in well-named, reusable functions.
Common use cases include fetching data, managing form state, subscribing to external data sources, implementing debounce/throttle logic, managing local storage, and handling complex UI interactions like drag-and-drop or keyboard shortcuts.
Code Example:
// Custom Hook for fetching data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
setData(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true; // Cleanup to prevent state updates on unmounted component
};
}, [url]);
return { data, loading, error };
}
// Using the custom Hook in multiple components
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user.name}</div>;
}
function PostList() {
const { data: posts, loading, error } = useFetch('/api/posts');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>;
}
Hook Composition Diagram:
graph TD
A[Component] --> B[useFetch]
B --> C[useState - data]
B --> D[useState - loading]
B --> E[useState - error]
B --> F[useEffect - fetch logic]
G[Component] --> H[useAuth]
H --> I[useContext - AuthContext]
H --> J[useLocalStorage - token]
H --> K[useEffect - token validation]
L[Component] --> M[useForm]
M --> N[useState - values]
M --> O[useState - errors]
M --> P[useCallback - handlers]
References:
↑ Back to top