React Hooks Explained Simply and Clearly

Summarize this article with:
Class components used to be the only way to manage state in React. Then hooks arrived and changed everything.
With React hooks explained properly, you can write cleaner functional components that handle state, side effects, and lifecycle events without the complexity of classes.
Facebook released hooks in React 16.8, and adoption exploded. There was a good reason for that.
This guide covers every built-in hook, from useState to useLayoutEffect. You will learn the rules, common mistakes, performance considerations, and how to build custom hooks.
Whether you are migrating from class components or starting fresh, this breakdown gives you the practical knowledge to use hooks with confidence.
What is a React Hook

A React Hook is a JavaScript function that lets you use state management and other React features inside functional components without writing class components.
Facebook introduced hooks in React 16.8 back in February 2019.
Before that release, you had to convert functional components to classes whenever you needed state or lifecycle methods. Honestly, it was tedious.
Hooks changed everything about how developers approach front-end development with React.
They provide a cleaner way to share stateful logic between components. No more render props or higher-order components cluttering your codebase.
The core idea: functions that “hook into” React’s internal features.
How Do React Hooks Work
Hooks maintain state through React’s internal tracking system, preserving values between renders using call order. According to Stack Overflow’s 2024 survey, 41.6% of professional developers use React, with Hooks being a core requirement for React jobs globally.
React tracks each hook call in sequence during render. This is why the Rules of Hooks exist (call them at top level only, not inside conditions or loops).
The render cycle determines when hooks execute. React compares values to decide if updates are needed.
Hook types by purpose:
- useState (local state)
- useEffect (side effects)
- useContext (context values)
- useReducer (complex state)
- useMemo/useCallback (performance)
- useRef (persistent references)
Understanding what React.js is helps you grasp why hooks fit the library’s design.
What is the useState Hook
useState declares state variables in functional components. Returns current value and setter function.
Syntax: const [count, setCount] = useState(0)
Implementation:
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
React batches multiple updates for performance. State updates trigger re-renders.
What is the useEffect Hook
useEffect handles side effects (data fetching, subscriptions, DOM manipulation) after render.
Runs after every render by default. Control execution with dependency array.
Common patterns:
// Run once on mount
useEffect(() => {
fetchData();
}, []);
// Run when dependency changes
useEffect(() => {
updateTitle(count);
}, [count]);
// Cleanup function
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer);
}, []);
Return cleanup functions to prevent memory leaks with subscriptions and timers.
What is the useContext Hook
useContext reads context provider values without prop drilling.
Pass the context object to useContext. Returns current context value.
For choosing between approaches, React context vs Redux compares tradeoffs for different app complexities.
What is the useReducer Hook
useReducer manages complex state logic with reducer functions, similar to Redux.
Syntax: const [state, dispatch] = useReducer(reducer, initialState)
Better than useState when state transitions depend on previous values or involve multiple sub-values.
What is the useCallback Hook
useCallback returns memoized callbacks that only change when dependencies change.
Research from Chrome DevTools Performance panel shows function reference caching can cut redundant rendering cycles by up to 80% in large-scale interfaces.
When to use:
- Passing callbacks to optimized child components
- Preventing child re-renders with React.memo
- Stabilizing function references in dependency arrays
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty deps with functional update
What is the useMemo Hook
useMemo caches expensive calculations between renders.
According to TSH.io performance testing, excessive useMemo usage can decrease performance by 5-11% during mounting. Use it only for computationally heavy operations.
Valid use cases:
- Calculations with datasets over 2,000 entries
- Expensive transformations (filtering large arrays, complex math)
- Derived values passed to memoized children
const filtered = useMemo(() =>
data.filter(item => item.active),
[data]
);
For broader optimization strategies, see React performance optimization techniques.
Performance benchmarks:
Research from performance testing shows frame rates jumping from under 40fps to steady 60fps when using memoization on interactive dashboards with large datasets. However, premature optimization adds complexity without benefits.
Implementation rule: Profile first with React DevTools Profiler, then optimize bottlenecks.
What is the useRef Hook
useRef creates mutable ref objects that persist across renders without triggering updates.
Access DOM elements with ref.current. Store previous values or mutable data that shouldn’t cause re-renders.
const inputRef = useRef(null);
const prevCount = useRef(count);
// Focus input
inputRef.current.focus();
What is the useLayoutEffect Hook
useLayoutEffect fires synchronously after DOM mutations but before browser paint.
Use for DOM measurements or visual updates that must happen before user sees changes. Blocks painting, so use sparingly.
When to use: Reading layout, synchronous DOM updates, preventing visual flicker.
What Are the Rules of React Hooks
React enforces two strict rules for hooks to maintain internal tracking.
Break these and component state becomes unpredictable. ESLint catches violations automatically.
Two mandatory rules:
- Call hooks at top level only
- Call hooks from React functions only
Setup ESLint protection:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
The eslint-plugin-react-hooks package ships with Create React App by default. According to developer case studies, fixing hook rule violations after the fact can take weeks. One team spent over a month correcting exhaustive-deps violations across their codebase.
Why Can Hooks Only Be Called at the Top Level
React tracks hooks by call order. Placing hooks in loops, conditions, or nested functions breaks tracking.
Same order every render. Always.
// Wrong - conditional hook
function Component() {
if (someCondition) {
const [state, setState] = useState(0);
}
}
// Correct - top level
function Component() {
const [state, setState] = useState(0);
if (someCondition) {
// Use state here
}
}
What breaks:
When hook order changes between renders, React loses track of state associations. The name !== ” example from React docs shows how skipping a hook shifts all subsequent hooks, causing the wrong state to be read.
Common violation patterns:
- Hooks inside if statements
- Hooks inside loops
- Hooks in event handlers
- Hooks in callbacks
ESLint warning example:
React Hook useState is called conditionally.
React Hooks must be called in the exact same order
in every component render.
Why Can Hooks Only Be Called in React Functions
Hooks rely on React’s internal component tracking. Regular JavaScript functions lack this context.
Valid hook locations:
- Functional components
- Custom hooks (functions starting with “use”)
// Wrong - regular function
function helperFunction() {
const [count, setCount] = useState(0);
}
// Correct - custom hook
function useCounter() {
const [count, setCount] = useState(0);
return { count, setCount };
}
// Correct - component
function Component() {
const { count, setCount } = useCounter();
}
Custom hook best practices:
According to React architecture patterns research, custom hooks reduce code duplication and complexity by separating common logic.
- Prefix with “use” (useAuth, useForm, useFetch)
- Group by domain, not hook type
- Extract when logic repeats across components
- Keep single responsibility
Custom hooks can call other hooks. Example: useAuth might use useLocalStorage internally.
When to create custom hooks:
- Logic repeated in multiple components
- Logic tied to React lifecycle (useEffect, useState)
- Separating logic improves readability
Enforcement through tooling:
The React team maintains eslint-plugin-react-hooks specifically for catching rule violations. Projects without proper ESLint configuration risk production bugs that take hours to debug, as documented in developer case studies where missing dependencies in useEffect caused race conditions reproducible only 1 in 75 page refreshes.
What is the Difference Between React Hooks and Class Components
Class components use this.state and lifecycle methods like componentDidMount. Hooks replace all of that with functions.
Adoption data from 2024 surveys:
According to Stack Overflow’s 2024 survey, 70% of developers favor functional components over class-based approaches. Research shows hooks increased productivity by 68% after adoption due to reduced boilerplate.
Key differences:
- No
thiskeyword confusion - Easier to share stateful logic
- Smaller bundle sizes (50-60% reduction)
- Better code coverage in testing
- Simpler mental model
Hooks don’t deprecate classes. Both work together in the same codebase.
Bundle Size Comparison
Performance testing reveals significant size differences.
Measured bundle sizes:
| Component Type | Minified Size |
|---|---|
| Class (with state) | 227.84 B |
| Hooks (with state) | 92.16 B |
Research shows functional components experience up to 30% decrease in bundle size, making them faster to load.
Why hooks are smaller:
- No class instance overhead
- No method binding
- Simpler transpiled code
- Less boilerplate
Code Structure Differences
// Class Component
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
render() {
return (
<button onClick={this.increment}>
{this.state.count}
</button>
);
}
}
// Hooks (same functionality)
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Hooks reduce code complexity by approximately 30%.
Developer Productivity Impact
Teams adopting functional components report measurable improvements.
Performance metrics from 2024-2025 surveys:
- 40% increase in team velocity
- 30% reduction in code complexity
- 25% project time savings through reusability
- 70% of junior developers report easier learning curve
Companies implementing hooks saw 25% increase in project throughput.
Logic Reuse
Class components challenges:
- Higher-Order Components (HOCs) create wrapper hell
- Render props become complex
- Logic splitting across lifecycle methods
- Difficult to share stateful logic
Hooks solution:
Custom hooks extract reusable logic. According to 2025 frontend surveys, 67% of engineers reported increased code reuse.
// Reusable custom hook
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => {
setValue(e.target.value);
};
return { value, onChange: handleChange };
}
// Use across components
function LoginForm() {
const email = useFormInput('');
const password = useFormInput('');
return (
<form>
<input {...email} type="email" />
<input {...password} type="password" />
</form>
);
}
Statistics show reusable code across projects can reach 40%, reducing redundancy.
Testing Benefits
Functional components with hooks are easier to unit test.
Testing advantages:
- Less internal complexity
- More predictable behavior
- No
thisbinding issues - Easier to mock dependencies
- Cleaner test setup
80% of developers favor declarative approach for managing side effects, making testing more straightforward.
State Management Comparison
Class components:
this.setState({ count: this.state.count + 1 });
Hooks:
setCount(count + 1);
// Or functional update
setCount(c => c + 1);
Usage statistics show 70% of developers report reduced complexity in state-related logic with hooks.
Performance Optimization
Both approaches support optimization, but hooks provide cleaner syntax.
Class components:
- shouldComponentUpdate
- PureComponent
- Manual binding optimization
Hooks:
- React.memo
- useMemo
- useCallback
Research indicates applications using functional patterns achieve 25% increase in performance metrics compared to class-based counterparts.
Migration Path
Gradual adoption is possible. No need to rewrite existing code.
Migration strategy:
- Start new components with hooks
- Convert components during refactors
- Keep classes where needed (error boundaries)
- Mix both in same codebase
Teams report 30% boost in development speed after adopting hooks for new features.
When to Use Each
Use hooks for:
- New projects (React team recommendation)
- Simpler state management
- Reusable logic patterns
- Smaller bundle sizes
- Functional programming preference
Keep classes for:
- Error boundaries (hooks don’t support yet)
- Legacy codebases requiring stability
- Third-party libraries requiring classes
60% of developers prefer functional constructs, while 40% still favor class approaches for specific scenarios.
Learning Curve
Research shows clear differences in onboarding.
Statistics from developer surveys:
- 70% of junior developers find hooks easier to learn
- 30% reduction in learning time
- Smoother onboarding for new team members
- Better collaboration through concise code
The functional approach aligns with modern JavaScript patterns, making it more accessible to newcomers.
What Are Custom Hooks in React

Custom hooks extract reusable logic into standalone functions that can use other hooks.
Name them starting with “use” so React recognizes them as hooks and applies the rules.
Developer impact data:
According to 2025 frontend surveys, 67% of engineers reported increased code reuse after implementing custom hooks. Projects can reduce development time by 25% through logic reusability.
How to Create a Custom Hook
Write a function starting with “use” that calls other hooks. Return whatever values components need.
Basic structure:
function useCustomHook() {
const [state, setState] = useState(initialValue);
useEffect(() => {
// Side effect logic
}, [dependencies]);
return { state, setState };
}
Naming convention rules:
- Must start with “use” (useForm, useAuth, useWindowSize)
- Use descriptive names indicating functionality
- ESLint enforces this convention automatically
Common Custom Hook Patterns
Data fetching hook:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
Form input hook:
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => {
setValue(e.target.value);
};
const reset = () => {
setValue(initialValue);
};
return {
value,
onChange: handleChange,
reset
};
}
// Usage
function LoginForm() {
const email = useFormInput('');
const password = useFormInput('');
return (
<form>
<input {...email} type="email" />
<input {...password} type="password" />
</form>
);
}
Local storage hook:
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
When Should You Use Custom Hooks
Extract logic when two or more components share the same stateful logic.
Decision checklist:
- Logic repeated in multiple components?
- Logic tied to React lifecycle (useEffect, useState)?
- Would separation improve readability or testability?
If yes to any question, create a custom hook.
Code reuse statistics:
Research shows reusable code across projects can reach 40%, reducing redundancy. Teams report reusable patterns can boost development speed by 30%.
Benefits of Custom Hooks
Code organization improvements:
- Promotes DRY (Don’t Repeat Yourself) principle
- Makes components leaner and focused on UI
- Encapsulates complex logic
- Easier to test in isolation
- Better code maintainability
Performance metrics from developer surveys:
- 70% reduction in code complexity for state-related logic
- Components become 30% more readable
- Testing becomes significantly easier
- Debugging time decreases substantially
Custom Hook Best Practices
1. Keep hooks focused
Encapsulate only one piece of logic per hook.
// Good - focused on one concern
function useAuth() {
const [user, setUser] = useState(null);
const login = (credentials) => { /* login logic */ };
const logout = () => { /* logout logic */ };
return { user, login, logout };
}
// Avoid - mixing multiple concerns
function useEverything() {
// Auth, data fetching, forms, etc.
}
2. Minimize state
Keep state minimal and focused.
3. Use TypeScript for type safety
function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, (val: T) => void] {
// Implementation
}
4. Handle dependencies correctly
Pay attention to useEffect dependency arrays.
5. Document your hooks
/**
* Manages window size state
* @returns {object} - { width, height }
*/
function useWindowSize() {
// Implementation
}
When NOT to Create Custom Hooks
Avoid premature abstraction:
- Logic used only once? Keep it in the component
- Too specific to one component? Don’t extract
- No lifecycle logic involved? Use regular function instead
Extract regular utility functions without React features.
// Not a hook - doesn't use React features
function formatCurrency(amount) {
return `$${amount.toFixed(2)}`;
}
// Is a hook - uses useState
function useCurrency(initialAmount) {
const [amount, setAmount] = useState(initialAmount);
const formatted = `$${amount.toFixed(2)}`;
return { amount, setAmount, formatted };
}
Testing Custom Hooks
Write unit tests for custom hooks.
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Testing ensures hooks work correctly and remain maintainable.
Composing Custom Hooks
Hooks can call other hooks.
function useAuth() {
const [user, setUser] = useLocalStorage('user', null);
const login = async (credentials) => {
const userData = await authenticate(credentials);
setUser(userData);
};
return { user, login };
}
This pattern allows building complex functionality from simple, focused hooks.
File Organization
Best practices for structure:
Group by domain, not hook type.
src/
hooks/
auth/
useAuth.js
usePermissions.js
forms/
useFormInput.js
useFormValidation.js
data/
useFetch.js
useCache.js
Organize related hooks together for better maintainability.
Common Use Cases
Popular custom hook examples:
useFetch– Data fetching with loading/error statesuseLocalStorage– Sync state with localStorageuseDebounce– Debounce value changesuseWindowSize– Track window dimensionsuseOnClickOutside– Detect clicks outside elementuseEventListener– Manage event listenersuseAuth– Authentication logicuseForm– Form state and validation
According to React best practices research, these patterns save significant development time across projects.
State Isolation
Each hook call gets isolated state.
function App() {
const counter1 = useCounter(); // Independent state
const counter2 = useCounter(); // Independent state
// counter1 and counter2 don't share state
}
Custom hooks reuse logic, not state itself.
Performance Considerations
Optimization tips:
- Use useMemo for expensive calculations
- Use useCallback for callback functions
- Minimize re-renders with proper dependencies
- Keep hooks simple and focused
Research indicates well-optimized hooks can improve application responsiveness by 25%.
Publishing Custom Hooks
Share hooks with the community.
Steps to publish:
- Bundle hooks into a library (use Rollup)
- Write documentation
- Add TypeScript types
- Publish to NPM
- Include usage examples
Popular libraries like react-use and @uidotdev/usehooks provide collections of production-ready custom hooks.
What Are the Benefits of Using React Hooks
- Cleaner code with less boilerplate than classes
- Logic extraction without changing component hierarchy
- Related code stays together instead of scattered across lifecycle methods
- Easier unit testing of isolated logic
- Better compatibility with TypeScript
- Gradual adoption alongside existing class components
Dan Abramov and the React team designed hooks to solve real frustrations developers faced daily.
What Are Common Mistakes When Using React Hooks
Most bugs come from dependency arrays and closure behavior. Get these right and hooks become predictable.
Impact on development:
Developer case studies show that missing dependencies can cause bugs that take hours to debug. One team spent weeks fixing exhaustive-deps violations across their codebase. According to experienced developers, these issues can run perfectly fine for months before producing hard-to-reproduce bug reports.
What is a Stale Closure in React Hooks
A stale closure captures outdated variable values from a previous render.
Happens when callbacks reference state without including it in dependencies. Your function sees old data while the component has moved on.
How stale closures occur:
function Counter() {
const [count, setCount] = useState(0);
// Wrong - captures stale count
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // Always logs 0
}, 1000);
return () => clearInterval(interval);
}, []); // Missing count dependency
return <button onClick={() => setCount(count + 1)}>
{count}
</button>;
}
The interval callback closes over count from the initial render. Even though count updates, the callback never sees the new value.
Fix with dependency array:
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // Now logs current count
}, 1000);
return () => clearInterval(interval);
}, [count]); // Include count
Alternative fix with functional updates:
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1); // No dependency on count needed
}, 1000);
return () => clearInterval(interval);
}, []); // Empty array is safe now
Functional updates receive the latest state value, avoiding stale closures entirely.
What Happens When Dependencies Are Missing in useEffect
Effects don’t re-run when they should. State and props become stale inside the effect.
Real-world example:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// Wrong - missing userId dependency
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Effect never updates when userId changes
return <div>{user?.name}</div>;
}
When userId prop changes, the effect doesn’t re-run. Component displays wrong user data.
Correct implementation:
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Runs when userId changes
The ESLint plugin exhaustive-deps catches this. Trust its warnings.
ESLint configuration:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
This comes with Create React App by default.
Common Mistake: Stale State in Consecutive Updates
State updates are asynchronous. Referencing state directly in multiple updates causes stale data.
// Wrong - both use same count value
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // Still uses old count
};
// Correct - functional updates
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1); // Uses latest value
};
Functional updates ensure each update sees the latest state.
Common Mistake: Objects and Arrays in Dependencies
Objects and arrays are compared by reference. New objects on each render cause infinite loops.
// Wrong - obj recreated on every render
function Component() {
const obj = { key: 'value' };
useEffect(() => {
doSomething(obj);
}, [obj]); // Infinite loop!
}
Solutions:
Move inside useEffect:
useEffect(() => {
const obj = { key: 'value' };
doSomething(obj);
}, []); // No dependency needed
Use useMemo:
const obj = useMemo(() => ({ key: 'value' }), []);
useEffect(() => {
doSomething(obj);
}, [obj]); // Safe - obj doesn't change
Move outside component:
const obj = { key: 'value' }; // Outside component
function Component() {
useEffect(() => {
doSomething(obj);
}, []); // Safe - obj is constant
}
Common Mistake: Conditional Hook Calls
Hooks must be called in the same order every render.
// Wrong - conditional hook call
function Component({ condition }) {
if (condition) {
const [state, setState] = useState(0);
}
return <div>...</div>;
}
This breaks React’s hook tracking.
Correct approach:
// Call hook at top level
function Component({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Use state here
}
return <div>...</div>;
}
Common Mistake: Not Cleaning Up Effects
Effects that subscribe to external systems need cleanup.
// Wrong - memory leak
useEffect(() => {
const subscription = subscribe(source);
// Missing cleanup
}, [source]);
// Correct - cleanup function
useEffect(() => {
const subscription = subscribe(source);
return () => {
subscription.unsubscribe();
};
}, [source]);
Cleanup prevents memory leaks and unexpected behavior.
Common Mistake: Overusing State
Not everything needs to be state.
// Wrong - derived value in state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // Unnecessary
// Correct - compute on render
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
Derived values should be computed, not stored.
Common Mistake: Ignoring ESLint Warnings
Developers disable the exhaustive-deps rule without understanding consequences.
// Wrong - silencing without fixing
useEffect(() => {
doSomething(prop);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Ignoring the problem
According to developer experience reports, this eventually causes bugs. Failed dependencies not caught during testing later require rewriting entire logic from scratch.
Better approach:
Understand why ESLint warns, then fix the root cause.
Debugging Stale Closures
Add logging to identify stale values:
useEffect(() => {
console.log('Effect running with count:', count);
const interval = setInterval(() => {
console.log('Interval sees count:', count);
}, 1000);
return () => clearInterval(interval);
}, [count]);
If interval logs don’t match effect logs, you have a stale closure.
Using useRef for Mutable Values
useRef holds mutable values without triggering re-renders.
function Timer() {
const [time, setTime] = useState(0);
const timeRef = useRef(time);
// Keep ref updated
useEffect(() => {
timeRef.current = time;
}, [time]);
useEffect(() => {
const interval = setInterval(() => {
// Always accesses current time
console.log(timeRef.current);
}, 1000);
return () => clearInterval(interval);
}, []); // Safe empty array
return <div>{time}</div>;
}
Refs provide access to current values without adding dependencies.
Common Mistake: Mutating State Directly
Never mutate state directly. Create new objects/arrays.
// Wrong - mutating array
const [items, setItems] = useState([]);
items.push(newItem); // Direct mutation
setItems(items); // React won't detect change
// Correct - new array
setItems([...items, newItem]);
Direct mutations don’t trigger re-renders.
Summary of Best Practices
Dependency arrays:
- Include ALL values referenced inside the effect
- Trust ESLint exhaustive-deps warnings
- Use functional updates to avoid dependencies
- Memoize objects/arrays if needed
Stale closures:
- Add proper dependencies
- Use functional state updates
- Consider useRef for mutable values
- Log values to identify stale data
General rules:
- Call hooks at top level only
- Clean up effects properly
- Don’t mutate state directly
- Compute derived values instead of storing them
Developer reports confirm that following these practices prevents hours of debugging and eliminates hard-to-reproduce bugs that can hide for months in production code.
What Are the Performance Considerations for React Hooks
Hooks themselves are fast. Performance problems come from misuse, not the hooks.
Focus on reducing unnecessary renders and expensive recalculations. Profile before optimizing.
How to Avoid Unnecessary Re-renders with Hooks
React components re-render when state or props change. This behavior is intentional and usually fast. Problems arise when re-renders cause unnecessary work.
Common causes of performance issues:
- Components re-rendering without prop/state changes
- Expensive calculations running on every render
- New function instances triggering child re-renders
- Large component trees re-rendering together
According to HTTP Archive data from 2024, the median JavaScript payload for desktop users exceeds 500 KB. Mobile users download significantly more. Every unnecessary render compounds this problem.
Use React.memo for Pure Components
React.memo prevents re-renders when props haven’t changed.
// Without memo - re-renders on every parent update
function ChildComponent({ data }) {
return <div>{data}</div>;
}
// With memo - only re-renders when data changes
const ChildComponent = React.memo(function ChildComponent({ data }) {
return <div>{data}</div>;
});
When to use React.memo:
- Component receives unchanging props frequently
- Component is expensive to render
- Component renders identical output with same props
When NOT to use React.memo:
- Component is cheap to render (fast components)
- Props change frequently
- Component always renders different content
React must compare props with React.memo, adding overhead. According to performance profiling data, this comparison can be more expensive than rendering if your component is already fast.
Wrap Callbacks with useCallback
Functions are recreated on every render. This causes issues when passing functions to memoized children.
function ParentComponent() {
const [count, setCount] = useState(0);
// Wrong - new function every render
const handleClick = () => {
setCount(count + 1);
};
// Correct - stable function reference
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty deps with functional update
return <MemoizedChild onClick={handleClick} />;
}
Profiling data from React DevTools shows that unstable handlers can increase child component render cycles by up to 60% in medium-scale applications.
Use functional state updates to avoid dependencies:
// Requires count in dependencies
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
// No dependency needed
const increment = useCallback(() => {
setCount(c => c + 1);
}, []);
When to use useCallback:
- Passing functions to memoized child components
- Functions used in dependency arrays of other hooks
- Event handlers in lists or frequently rendered components
When NOT to use useCallback:
- Functions only used internally
- Simple event handlers not passed as props
- Functions that change frequently anyway
Developer reports show that wrapping expensive functions with useCallback maintains the same reference between renders, cutting redundant rendering cycles by up to 80% in large-scale single-page applications.
Cache Computed Values with useMemo
useMemo memoizes expensive calculations.
function ProductList({ products }) {
const [theme, setTheme] = useState("light");
// Wrong - recalculates on every render
const visibleProducts = products
.filter(p => p.inStock)
.sort((a, b) => a.price - b.price)
.map(p => ({ ...p, label: `${p.name} ($${p.price})` }));
// Correct - only recalculates when products change
const visibleProducts = useMemo(() => {
return products
.filter(p => p.inStock)
.sort((a, b) => a.price - b.price)
.map(p => ({ ...p, label: `${p.name} ($${p.price})` }));
}, [products]);
return (
<>
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
Toggle theme
</button>
<ul>
{visibleProducts.map(p => (
<li key={p.id}>{p.label}</li>
))}
</ul>
</>
);
}
When theme changes, the calculation doesn’t re-run. The component uses the cached value.
Performance testing on interactive dashboards with datasets exceeding 2,000 entries shows frame rate stability jumping from under 40 fps to a steady 60 fps using useMemo for filtering logic. Chrome DevTools Performance panel measurements confirm a 45% reduction in computation time.
When to use useMemo:
- Expensive calculations (data transformations, filtering, sorting)
- Search features that filter large datasets
- Complex computations that don’t need to run every render
When NOT to use useMemo:
- Simple calculations (already very fast)
- Values that change on every render
- Premature optimization without profiling
Split State to Avoid Cascading Updates
Keep state as local as possible. Lifting state unnecessarily causes cascading re-renders.
// Wrong - global state causes all children to re-render
function App() {
const [formData, setFormData] = useState({
name: '',
email: '',
address: '',
city: ''
});
// All fields re-render when any field changes
}
// Correct - split state by component
function NameField() {
const [name, setName] = useState('');
// Only this component re-renders
}
function EmailField() {
const [email, setEmail] = useState('');
// Only this component re-renders
}
State organization best practices:
- Keep state close to where it’s used
- Don’t lift state higher than necessary
- Split unrelated state into separate useState calls
- Use Context selectors for large contexts
Use React DevTools Profiler to Identify Bottlenecks
Always profile before optimizing.
React DevTools Profiler measures component render performance.
How to use the Profiler:
- Open React DevTools → Profiler tab
- Click Record
- Interact with your application
- Stop recording
- Analyze the flame chart
What to look for:
- Components with long render times (yellow/red bars)
- Components that re-render frequently
- Why each component rendered (enable in settings)
The Profiler shows:
- Render duration for each component
- Number of times each component rendered
- What triggered the render (props changed, state changed, parent rendered)
Programmatic profiling:
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime
) {
console.log(`${id} took ${actualDuration}ms to render`);
}
<Profiler id="Dashboard" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
This tracks render timing in production builds.
Additional Performance Tools
Chrome DevTools Performance tab:
- Shows JavaScript execution time
- Identifies main thread blocking
- Tracks layout and paint times
- Measures Core Web Vitals
Why Did You Render library:
Adds console warnings when components re-render without prop/state changes. Especially useful with React.memo.
npm install @welldone-software/why-did-you-render
Webpack Bundle Analyzer:
Visualizes bundle size and identifies large dependencies.
npm install --save-dev webpack-bundle-analyzer
Shows which modules take up the most space.
Code Splitting and Lazy Loading
Split code into smaller chunks. Load components only when needed.
import { lazy, Suspense } from 'react';
// Everything loads upfront
import Dashboard from './Dashboard';
import AdminPanel from './AdminPanel';
// Load on demand
const Dashboard = lazy(() => import('./Dashboard'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
);
}
When to split:
- Route-level components (different pages)
- Large components (>10KB)
- Rarely used features (admin panels, settings)
- Heavy libraries (charts, rich text editors)
When NOT to split:
- Small components (<10KB)
- Components needed on initial load
- Critical above-the-fold content
Next.js and similar frameworks provide automatic route-level splitting.
List Virtualization
Render only visible items in long lists.
npm install react-window
import { FixedSizeList } from 'react-window';
function LargeList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{Row}
</FixedSizeList>
);
}
Only renders items in the viewport. Handles lists with thousands of items efficiently.
Use for lists with 100+ items or complex list items (cards with images, charts).
Avoid Common Performance Pitfalls
Don’t create components inside render functions:
// SEVERE PERFORMANCE KILLER
function Parent() {
const ChildComponent = () => <div>Child</div>;
return <ChildComponent />; // React re-mounts on every render
}
// Define outside
const ChildComponent = () => <div>Child</div>;
function Parent() {
return <ChildComponent />;
}
React re-mounts components defined inside render, destroying and recreating them from scratch.
Don’t use anonymous functions in JSX (when passing to memoized children):
// New function every render
<MemoizedChild onClick={() => handleClick()} />
// Stable reference
const handleClick = useCallback(() => {
// handle click
}, []);
<MemoizedChild onClick={handleClick} />
Use stable, unique keys in lists:
// Wrong - array indices aren't stable
{items.map((item, index) => (
<Item key={index} {...item} />
))}
// Correct - use unique IDs
{items.map(item => (
<Item key={item.id} {...item} />
))}
Testing Optimization Strategies
Proper mocking in unit tests helps verify your optimization strategies work as expected.
Test that:
- Memoized components don’t re-render with same props
- Callbacks maintain referential equality
- Lazy components load correctly
- Performance regressions don’t occur
import { render } from '@testing-library/react';
test('memoized component does not re-render with same props', () => {
const { rerender } = render(<MemoizedComponent data="test" />);
// Re-render with same props
rerender(<MemoizedComponent data="test" />);
// Verify component didn't re-render
});
React 19 Compiler and Automatic Optimization
React 19 introduces a compiler that automatically handles memoization.
The compiler:
- Analyzes components and optimizes them
- Eliminates need for manual useMemo/useCallback in most cases
- Prevents unnecessary re-renders automatically
- Makes code cleaner without performance tradeoffs
Early adopters report 25-40% fewer re-renders in complex applications without code changes. One dashboard application eliminated 2,300 lines of memoization code while running faster.
For apps using React 19+, write simple code first and let React optimize it. Use manual memoization only when truly necessary.
Key Takeaways
Profile first, optimize later.
- Use React DevTools Profiler to find real bottlenecks
- Measure before and after optimization
- Don’t optimize without data
Apply optimizations strategically.
- React.memo for expensive, frequently re-rendered components
- useCallback for callbacks passed to memoized children
- useMemo for expensive calculations only
- Code splitting for routes and large features
Avoid premature optimization.
Every optimization adds complexity and overhead. According to Kent C. Dodds, the cost of using useCallback and useMemo improperly can outweigh their benefits.
Keep it simple.
- Split state to reduce re-renders
- Keep components small and focused
- Don’t create components in render functions
- Use stable keys in lists
Performance optimization is iterative. Profile, identify bottlenecks, apply targeted fixes, and measure results.
What Are the Practical Examples of React Hooks
Seeing hooks in real code makes the concepts click faster than theory alone.
useState Example
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
useEffect Example
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
}, [userId]);
return user ? <h1>{user.name}</h1> : <p>Loading...</p>;
}
Custom Hook Example
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
These patterns appear constantly in production React applications. Master them and you handle most real-world scenarios.
For larger applications, explore popular React libraries that complement hooks with routing, state management, and data fetching solutions.
FAQ on React Hooks
What are React hooks used for?
React hooks let you use state management and lifecycle features in functional components. They replace class component patterns with simpler functions for handling local state, side effects, context, and refs.
Can I use hooks in class components?
No. Hooks only work inside functional components or custom hooks. Class components use this.state and lifecycle methods instead. You can mix both component types in the same codebase during gradual migration.
What is the difference between useState and useReducer?
The useState hook handles simple state with direct updates. useReducer manages complex state logic through a reducer function and dispatch actions. Choose useReducer when state transitions depend on previous values.
Why does useEffect run twice in development?
React 18 Strict Mode intentionally double-invokes effects to help detect bugs. This only happens in development. Your cleanup function should handle this correctly. Production builds run effects once as expected.
What causes infinite loops with useEffect?
Missing or incorrect dependency arrays cause infinite loops. If you update state inside useEffect without proper dependencies, the component re-renders endlessly. Always include all values your effect reads in the array.
When should I use useMemo vs useCallback?
Use useMemo to cache computed values from expensive calculations. Use useCallback to memoize callback functions passed to child components. Both prevent unnecessary recalculations and re-renders when dependencies stay unchanged.
Are React hooks better than class components?
Hooks offer cleaner syntax, easier logic reuse, and smaller bundles. Class components still work fine. Most developers prefer hooks for new code. The React team recommends hooks for React beginners starting fresh.
How do I share logic between components using hooks?
Create custom hooks by extracting shared logic into functions starting with “use”. These functions can call other hooks and return values. Components using the same custom hook get independent state instances.
Can I call hooks conditionally?
No. React tracks hooks by call order. Conditional hook calls break this tracking and cause bugs. Always call hooks at the top level of your component, then use conditions inside the hook logic.
How do I test components that use hooks?
Use React testing libraries like React Testing Library or Enzyme. Test component behavior, not hook implementation. For custom hooks specifically, the @testing-library/react-hooks package provides dedicated utilities.
Conclusion
With React hooks explained throughout this guide, you now have a solid foundation for building modern functional components.
The shift from class components to hooks simplifies state initialization, effect dependencies, and logic abstraction. Your code becomes easier to read and test.
Start with useState and useEffect. Master those before moving to useReducer, useMemo, and useCallback.
Build custom hooks to extract reusable logic across your application. Use the React DevTools profiler to catch performance issues early.
Remember the rules: top-level calls only, React functions only.
Hooks work seamlessly with TypeScript and integrate well with libraries like Redux and React Router. The ecosystem supports you.
Now go build something.
- Feature-Driven Development vs Agile: Key Differences - March 12, 2026
- Agile vs DevOps: How They Work Together - March 11, 2026
- Ranking The Best Mapping Software by Features - March 11, 2026







