“React Hooks turned complex class components into simple, reusable functions.”
Key Takeaways
- React Hooks replaced class components as the standard way to write React.
- useState, useEffect, and useContext are used in almost every production React app.
- Performance hooks like useMemo, useCallback, and useTransition prevent unnecessary re-renders.
- useReducer is better than useState for complex state logic.
- Custom hooks allow reusable business logic across components.
- React 18 introduced concurrent features like useTransition and useDeferredValue.
- Hooks must follow the Rules of Hooks to avoid bugs and performance issues.
Index
- Introduction to React Hooks
- Quick Reference - All Hooks
- State Management Hooks
- Context Hook
- Effect Hooks
- Ref Hooks
- Performance Hooks
- React 18 - Transition Hooks
- Utility Hooks
- React 19 - Form and Optimistic Hooks
- Custom Hooks
- Rules of Hooks
- Stats
- Interesting Facts
- FAQ's
- Conclusion
Introduction to React Hooks
React Hooks were introduced in React 16.8 (released February 2019) and fundamentally transformed how developers build components. Before hooks, building stateful or interactive React components required writing class components with lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount - verbose, often confusing, and difficult to share logic between components.
Hooks change all of that. They let you use state, side effects, context, and other React features directly inside plain JavaScript functions - no class required.
Here is how each major React version expanded the hooks system:
The real power lies not just in knowing hooks, but in knowing when to use them, when not to, and how to combine them effectively.
Quick Reference - All Hooks at a Glance
“Hooks are not just features in React - they are the foundation of modern React development.”
State Management Hooks
These hooks store and manage data inside your component. Start here - you will use them in every React app you build.
1. useState - Local Component State
The most fundamental hook. Use it to store any simple value that should trigger a re-render when it changes - booleans, strings, numbers, arrays, or objects.
const [count, setCount] = useState(0);
<button onClick={() => setCount(prev => prev + 1)}>
Clicked {count} times
</button>
Real-Life Example: Light Switch
import { useState } from 'react';
function LightSwitch() {
const [isOn, setIsOn] = useState(false);
return (
<button onClick={() => setIsOn(!isOn)}>
{isOn ? 'Light ON' : 'Light OFF'}
</button>
);
}
Common practical uses:
- Form inputs and controlled components
- Toggle dark/light theme
- Loading and error state management
- Modal open/close state
2. useReducer - Complex State Logic
Think of useReducer as mini-Redux inside a single component. When state logic becomes complex or involves multiple related values that change together, reach for useReducer instead of stacking multiple useStates.
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
case 'reset': return { count: 0 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</>
);
}
When to use useReducer over useState:
- State logic involves multiple sub-values
- The next state depends on the previous one in complex ways
- Multi-step forms with validation
- Shopping cart logic with add, remove, and update quantity
Context Hook
Context lets you share data globally across your component tree without passing props through every level (prop drilling).
3. useContext - Global State Without Redux
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
function App() {
return (
<ThemeContext.Provider value='dark'>
<Button />
</ThemeContext.Provider>
);
}
Perfect for:
- Authentication - storing the logged-in user globally
- Theme management - dark/light mode across the entire app
- Language and i18n switching
- App-wide notifications or toast messages For small-to-medium apps, useContext can fully replace Redux. For very large apps with complex state interactions, consider Redux or Zustand.
Effect Hooks
Effect hooks let you run code that interacts with things outside of React - the browser, external APIs, timers, and subscriptions.
4. useEffect - Side Effects After Render
Runs after the component renders. Use it for anything that needs to happen as a consequence of rendering - API calls, subscriptions, DOM updates, and timers.
// Runs once on mount (empty dependency array)
useEffect(() => {
fetchUsers();
}, []);
// Runs every time 'userId' changes
useEffect(() => {
fetchUserDetails(userId);
}, [userId]);
// Cleanup example -- WebSocket or timer
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer); // cleanup on unmount
}, []);
Common uses:
- API calls on component mount or when data changes
- WebSocket and real-time subscriptions
- Timers and intervals
- Updating the document title
- Most Common Mistake: A wrong or missing dependency array causes infinite re-renders. Always include every variable used inside the effect in the dependency array.
5. useLayoutEffect - Before the Browser Paints
Identical to useEffect, but fires synchronously after DOM mutations and before the browser paints pixels to screen. Use this when you need to measure or adjust the DOM to prevent a visible flicker.
import { useLayoutEffect, useRef, useState } from 'react';
function SquareBox() {
const boxRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
const width = boxRef.current.offsetWidth;
setHeight(width); // make it a perfect square
}, []);
return (
<div ref={boxRef} style={{ width: '200px', height }}>
Perfect Square
</div>
);
}
Use useLayoutEffect when:
- Measuring DOM dimensions before showing the component
- Preventing a visible layout flicker or jump
- Animating elements that need precise initial position data
6. useInsertionEffect - For CSS Library Authors
Runs before layout and painting to inject styles into the document. This is a very low-level hook used internally by CSS-in-JS libraries like Styled-components and Emotion. You will rarely need this in application code.
import { useInsertionEffect } from 'react';
function StyledComponent() {
useInsertionEffect(() => {
const style = document.createElement('style');
style.innerHTML = '.my-class { color: black; }';
document.head.appendChild(style);
}, []);
return <p className='my-class'>Hello</p>;
}
Ref Hooks
Ref hooks let you hold a mutable value that persists between renders without triggering a re-render, and give you a way to directly access DOM nodes.
7. useRef - DOM Access and Persistent Values
Refs are like a box where you can put any value. Changing a ref does NOT cause a re-render. Two common uses: accessing a DOM element directly, or storing a value that needs to persist across renders.
// Use case 1: Access DOM element
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Focus on mount
}, []);
<input ref={inputRef} />
// Use case 2: Persist value without causing re-render
const renderCount = useRef(0);
renderCount.current += 1;
Practical uses:
- Programmatically focus input fields
- Store the previous value of a prop or state
- Hold a timer ID without triggering re-renders
- Integrate with non-React third-party DOM libraries
8. useImperativeHandle - Expose Child Methods to Parent
Customises what a parent component can access when it uses a ref on a child. Used together with forwardRef.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focusInput() { inputRef.current.focus(); },
clearInput() { inputRef.current.value = ''; }
}));
return <input ref={inputRef} />;
});
function Parent() {
const inputRef = useRef();
return (
<>
<CustomInput ref={inputRef} />
<button onClick={() => inputRef.current.focusInput()}>
Focus Child
</button>
</>
);
}
Performance Hooks
React re-renders components whenever state or props change. These hooks let you control exactly what gets recalculated or recreated, preventing unnecessary work.
9. useMemo - Memoize Expensive Calculations
Caches the result of an expensive calculation and only recomputes it when its dependencies change.
import { useMemo } from 'react';
function ProductList({ products, category }) {
const filtered = useMemo(() => {
return products.filter(p => p.category === category);
}, [products, category]);
return filtered.map(p => <div key={p.id}>{p.name}</div>);
}
Do not overuse useMemo. It has its own overhead. Only add it when you have measured a real performance problem.
10. useCallback - Memoize Function References
Returns a memoized version of a function that only changes if its dependencies change. Prevents child re-renders caused by a new function reference on every parent render.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked!');
}, []); // Same function reference every render
return <ChildComponent onClick={handleClick} />;
}
React 18 Transition Hooks
React 18 introduced concurrent rendering - the ability to work on multiple state updates simultaneously and prioritise the most important ones.
11. useTransition - Non-Blocking UI Updates
Marks a state update as non-urgent so React can keep the UI responsive while processing it in the background.
import { useState, useTransition } from 'react';
function SearchPage({ allItems }) {
const [query, setQuery] = useState('');
const [list, setList] = useState(allItems);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
setQuery(e.target.value); // Urgent: update input immediately
startTransition(() => {
setList(allItems.filter(i => i.includes(e.target.value)));
});
}
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>Loading results...</p>}
{list.map(item => <p key={item}>{item}</p>)}
</>
);
}
12. useDeferredValue - Delay a Value Update
Accepts a value and returns a deferred version of it. When the value changes rapidly, the deferred version lags slightly, letting the UI stay responsive.
import { useState, useDeferredValue } from 'react';
function SearchList({ items }) {
const [search, setSearch] = useState('');
const deferredSearch = useDeferredValue(search);
const filtered = items.filter(item => item.includes(deferredSearch));
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filtered.map(item => <p key={item}>{item}</p>)}
</>
);
}
Utility Hooks
13. useId - Unique Accessible IDs
Generates a stable unique ID consistent between server and client rendering. Essential for accessibility - correctly linking HTML labels to their inputs.
“Small hooks, powerful logic - that’s the beauty of modern React.”
import { useId } from 'react';
function FormField({ label }) {
const id = useId();
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} />
</>
);
}
14. useSyncExternalStore - Subscribe to External Data
The correct way to subscribe to external data sources like browser APIs or Redux store.
import { useSyncExternalStore } from 'react';
function subscribe(callback) {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}
function WindowWidth() {
const width = useSyncExternalStore(subscribe, () => window.innerWidth);
return <p>Window is {width}px wide</p>;
}
15. useDebugValue - Debug Custom Hooks
Adds a label to a custom hook that appears in React DevTools. Only useful inside custom hooks for debugging - no effect on production behaviour.
import { useState, useDebugValue } from 'react';
function useOnlineStatus() {
const [isOnline] = useState(true);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
React 19 Form and Optimistic Hooks
React 19 introduces hooks designed around server actions and forms, making form handling and optimistic UI significantly simpler.
16. useFormStatus - Know If a Form Is Submitting
Must be used inside a component that is a child of a form element. Gives access to the parent form's submission state.
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
17. useFormState - Manage Form Validation State
Manages state that comes back from a form action - like validation errors or success messages.
'use client';
import { useFormState } from 'react-dom';
async function validateName(prevState, formData) {
if (!formData.get('name')) return { error: 'Name is required' };
return { success: true };
}
function MyForm() {
const [state, formAction] = useFormState(validateName, {});
return (
<form action={formAction}>
<input name='name' />
{state.error && <p>{state.error}</p>}
<button>Submit</button>
</form>
);
}
18. useOptimistic - Instant UI Before Server Confirms
Shows an optimistic (assumed-successful) version of the UI immediately before the server has confirmed the operation. Rolls back automatically on failure.
'use client';
import { useOptimistic, useState } from 'react';
function CommentSection() {
const [comments, setComments] = useState([]);
const [optimisticComments, addOptimistic] = useOptimistic(
comments,
(state, newComment) => [...state, { text: newComment, pending: true }]
);
async function addComment(formData) {
const text = formData.get('comment');
addOptimistic(text);
await postCommentToServer(text);
setComments(prev => [...prev, { text }]);
}
return (
<>
<form action={addComment}>
<input name='comment' />
<button>Add Comment</button>
</form>
{optimisticComments.map((c, i) => (
<p key={i}>{c.text}</p>
))}
</>
);
}
Custom Hooks
Custom hooks let you extract and reuse stateful logic across multiple components. They are simply JavaScript functions that start with 'use' and can call other hooks.
If you find yourself copy-pasting the same useEffect and useState pattern across components, it belongs in a custom hook.
“Custom Hooks are where React truly becomes scalable and elegant.”
Example 1: useFetch - Reusable Data Fetching
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
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 };
}
const { data: users, loading } = useFetch('/api/users');
Example 2: useDebounce - Optimised Search Input
import { useState, useEffect } from 'react';
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
const debouncedQuery = useDebounce(searchQuery, 300);
useEffect(() => { searchAPI(debouncedQuery); }, [debouncedQuery]);
Example 3: useToggle - Reusable Boolean Toggle
import { useState } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(prev => !prev);
return [value, toggle];
}
const [isOpen, toggle] = useToggle();
const [isDark, toggleDark] = useToggle(false);
Example 4: useAuth - Authentication State
import { useContext } from 'react';
function useAuth() {
const { user, setUser } = useContext(AuthContext);
const login = async (credentials) => {
const data = await loginAPI(credentials);
setUser(data.user);
localStorage.setItem('token', data.token);
};
const logout = () => {
setUser(null);
localStorage.removeItem('token');
};
return { user, isAuthenticated: !!user, isAdmin: user?.role === 'admin', login, logout };
}
Rules of Hooks
React relies on the order in which hooks are called being stable between renders. Breaking these rules causes subtle bugs that are very hard to track down.
Stats
- According to the Stack Overflow Developer Survey 2023, React remains one of the most widely used web frameworks worldwide.
- Source: https://survey.stackoverflow.co/2023/
- React has 200,000+ stars on GitHub, making it one of the most popular open-source libraries.
- Source: https://github.com/facebook/react
- According to State of JS survey, React continues to rank among the most adopted frontend frameworks.
- Source: https://stateofjs.com/
Interesting Facts
- React Hooks were proposed by Dan Abramov and Sebastian Markbåge.
- Hooks eliminated the need for most class components.
- Hooks enabled the creation of custom hooks, which allow logic reuse without higher-order components or render props.
- Libraries like React Query, Zustand, and TanStack Table heavily rely on hooks internally.
- Hooks follow strict rules because React depends on the order of hook calls to manage component state efficiently.
FAQ's
1. Are class components deprecated?
No. Class components still work in React, but modern React development almost always uses hooks.
2. Can hooks replace Redux?
For small to medium applications, useContext+useReducer can replace Redux.
However, large applications often benefit from dedicated state libraries like Redux, Zustand, or Jotai.
3. Why must hooks follow strict rules?
React internally tracks hook calls based on their order during each render.
Calling hooks conditionally breaks this order and causes unpredictable behavior.
4. Can custom hooks call other hooks?
Yes - that’s the main idea behind them.
Conclusion
React Hooks fundamentally changed how developers build React applications.
Instead of writing complex class components with lifecycle methods, developers can now manage state, side effects, performance optimizations, and global data using small composable functions.
Learning when to use each hook - and when not to - is one of the most important skills for becoming a confident React developer.
Start with the core hooks: useState, useEffect, useContext, and useRef.
Once comfortable, explore advanced hooks like useMemo, useCallback, and React 18’s concurrent features.
And finally, begin writing your own custom hooks - this is where React truly becomes powerful and scalable.
About the Author:Vatsal is a web developer at AddWebSolution. Building web magic with Laravel, PHP, MySQL, Vue.js & more.




Top comments (0)