Every React Hook, Catalogued: From useState to Custom Logic Reuse
React Hooks Complete Guide: From Built-in Hooks to Custom Hooks, Master Them All in One Article
React 16.8 introduced Hooks, giving function components all the capabilities of class components. This article will systematically teach you the usage of all built-in Hooks and how to encapsulate your own custom Hooks, making code reuse effortless.
Preface: Why Do We Need Hooks?
Before React 16.8, function components could only receive props and render UI; they couldn't manage state or handle side effects. If you needed those capabilities, you had to write class components.
The emergence of Hooks completely changed this situation. It allows developers to use React's state and other features without writing class components, greatly expanding the capabilities of function components. From then on, the functional programming paradigm gained wider adoption in React.
Core Value of Hooks
- Logic Reuse: Extract component logic into reusable functions, not just UI reuse
- Code Clarity: Break complex components into multiple small functions, each focused on one thing
- Performance Optimization: Avoid unnecessary computations and renders through Hooks like
useMemoanduseCallback
1. The Golden Rules of Hooks
Hooks look like ordinary JavaScript functions, but they represent a special type of reusable UI logic with strict restrictions on where they can be called.
Rule 1: Only Call Hooks at the Top Level
Do not call Hooks inside loops, conditionals, nested functions, or try/catch/finally blocks. Always call Hooks at the top level of your React function component, before any return.
// ❌ Wrong: calling inside a conditional
function Bad({ cond }) {
if (cond) {
const theme = useContext(ThemeContext); // Forbidden!
}
}
// ❌ Wrong: calling inside a loop
function Bad() {
for (let i = 0; i < 10; i++) {
const theme = useContext(ThemeContext); // Forbidden!
}
}
// ❌ Wrong: calling after a conditional return
function Bad({ cond }) {
if (cond) return;
const theme = useContext(ThemeContext); // Forbidden!
}
// ❌ Wrong: calling inside an event handler
function Bad() {
function handleClick() {
const theme = useContext(ThemeContext); // Forbidden!
}
}
// ❌ Wrong: calling inside a class component
class Bad extends React.Component {
render() {
useEffect(() => {}); // Forbidden!
}
}
// ✅ Correct: calling at the top level of a function component
function Good() {
const [count, setCount] = useState(0); // ✅
const theme = useContext(ThemeContext); // ✅
// ...
}
Why? React relies on the call order of Hooks to correctly preserve the state of each Hook. If Hooks are called conditionally, the order gets disrupted, leading to state mismatches.
Rule 2: Only Call Hooks from React Functions
- ✅ Call them in React function components
- ✅ Call them in custom Hooks
- ❌ Do not call them in ordinary JavaScript functions
// ❌ Wrong: calling in an ordinary JS function
function normalFunction() {
const [count, setCount] = useState(0); // Forbidden!
}
// ✅ Correct: calling in a custom Hook
function useCustomHook() {
const [count, setCount] = useState(0); // ✅ Custom Hooks can call other Hooks
return count;
}
Note: Custom Hooks can call other Hooks — that's the entire point of their existence. Because custom Hooks are also only called when the function component renders.
2. Overview of Built-in Hook Categories
React provides over ten built-in Hooks, which can be grouped by functionality:
| Category | Hooks | Purpose |
|---|---|---|
| State Hooks | useState, useReducer |
Manage component state |
| Context Hooks | useContext |
Read and subscribe to context |
| Ref Hooks | useRef, useImperativeHandle |
Hold information not used for rendering |
| Effect Hooks | useEffect, useLayoutEffect, useInsertionEffect |
Connect to external systems |
| Performance Hooks | useMemo, useCallback |
Skip computations and unnecessary re-renders |
| Other Hooks | useTransition, useDeferredValue, useId, useSyncExternalStore, useDebugValue |
Concurrent rendering, debugging, and other special scenarios |
3. State Hooks: State Management
1. useState — The Most Basic State Hook
useState lets you add state to function components. It returns a state value and a function to update that state value.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initial value is 0
return (
<div>
<p>Clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Lazy Initialization: If the initial state requires expensive computation, you can pass a function:
const [state, setState] = useState(() => {
const expensiveResult = someExpensiveComputation();
return expensiveResult;
});
2. useReducer — Complex State Logic
When state update logic is complex, involves multiple sub-values, or depends on previous state, useReducer is a better choice than useState.
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</>
);
}
4. Context Hook: Passing Data Across Levels
useContext — Escape Prop Drilling
useContext lets a component receive information from an ancestor component without passing it as props through every level.
import { useContext, createContext } from 'react';
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <Button />;
}
function Button() {
const theme = useContext(ThemeContext); // Read directly, no prop drilling
return <button className={theme}>Button</button>;
}
5. Ref Hooks: Holding Data Not Used for Rendering
1. useRef — A "Storage Box" That Doesn't Trigger Renders
Refs let a component hold information not used for rendering, like DOM nodes or timeout IDs. Unlike state, updating a ref does not trigger a component re-render.
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // Auto-focus after mount
}, []);
return <input ref={inputRef} type="text" />;
}
2. useImperativeHandle — Customize the Ref Exposed to the Parent
useImperativeHandle lets you customize the instance value or methods exposed to the parent component when using refs. This is useful when you want to limit the parent's access to the child component.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const ChildInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// Only expose focus and clear methods to the parent, not the entire DOM node
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} />;
});
function Parent() {
const childRef = useRef(null);
return (
<>
<ChildInput ref={childRef} />
<button onClick={() => childRef.current.focus()}>Focus Child Input</button>
</>
);
}
6. Effect Hooks: Connecting to External Systems
1. useEffect — The Most Commonly Used Side-Effect Hook
useEffect lets a component connect to and synchronize with external systems. This includes data fetching, subscription management, DOM manipulation, and more.
import { useState, useEffect } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Side effect: connect to chat room
const connection = createConnection(roomId);
connection.connect();
connection.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
// Cleanup function: disconnect when component unmounts or roomId changes
return () => {
connection.disconnect();
};
}, [roomId]); // Dependency array: re-run when roomId changes
return <div>{/* Render message list */}</div>;
}
Three Cases for the Dependency Array:
useEffect(() => {}, [])— Run once on mountuseEffect(() => {}, [a, b])— Run when a or b changesuseEffect(() => {})— Run after every render (use with caution!)
Important Reminder: Effects are an "escape hatch" in the React paradigm. If you don't need to interact with an external system, you probably don't need an Effect.
2. useLayoutEffect — Execute Synchronously Before the Browser Paints
useLayoutEffect is similar to useEffect, but it executes synchronously before the browser repaints the screen. It's suitable for measuring layout, preventing flickers, and similar scenarios.
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
// Measure DOM height before the browser paints to avoid flickering
useLayoutEffect(() => {
if (ref.current) {
setHeight(ref.current.getBoundingClientRect().height);
}
}, []);
return <div ref={ref}>Height is: {height}px</div>;
}
useEffectvsuseLayoutEffect:useEffectexecutes asynchronously and suits most scenarios;useLayoutEffectexecutes synchronously and suits operations that need to run immediately after layout updates.
7. Performance Hooks: Performance Optimization
1. useMemo — Cache Computed Results
useMemo returns a memoized value, avoiding repeated computation on every render.
import { useMemo, useState } from 'react';
function ExpensiveComponent({ a, b }) {
const [count, setCount] = useState(0);
// Only recompute when a or b changes; otherwise use the cached value
const expensiveResult = useMemo(() => {
console.log('Running expensive computation...');
return a * b * 1000; // Simulate complex computation
}, [a, b]);
return (
<div>
Result: {expensiveResult}
<button onClick={() => setCount(count + 1)}>Unrelated state: {count}</button>
</div>
);
}
2. useCallback — Cache Function References
useCallback returns a memoized callback function, preventing unnecessary re-renders.
import { useCallback, useState } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Only create a new function when dependencies change; otherwise reuse the previous one
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Empty dependencies → never changes
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Parent state: {count}</button>
</>
);
}
useMemovsuseCallback:useMemocaches values,useCallbackcaches functions. Both are for performance optimization, but don't overuse them — only use them when there is a genuine performance problem.
8. New Hooks in React 18
React 18 introduced several new Hooks, primarily around concurrent rendering and server-side rendering.
1. useTransition — Mark Non-Urgent Updates
useTransition lets you mark certain state updates as non-urgent. By default, other state updates are considered urgent, and React allows urgent updates (like typing in an input) to interrupt non-urgent updates (like rendering a search results list).
import { useState, useTransition } from 'react';
function SearchPage() {
const [input, setInput] = useState('');
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value); // Urgent update: update the input immediately
startTransition(() => {
// Non-urgent update: can be interrupted
const newList = generateLargeList(value);
setList(newList);
});
};
return (
<div>
<input value={input} onChange={handleChange} />
{isPending && <span>Loading...</span>}
<ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul>
</div>
);
}
2. useDeferredValue — Defer a Non-Urgent Value
useDeferredValue lets you defer re-rendering a non-urgent part of the tree. It's similar to debouncing but has no fixed time delay — React will attempt the deferred render immediately after the first render completes.
import { useState, useDeferredValue, useMemo } from 'react';
function SearchPage() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
// Only recompute when deferredText changes, and the computation can be interrupted
const list = useMemo(() => {
return generateLargeList(deferredText);
}, [deferredText]);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<List items={list} />
</div>
);
}
3. useId — Generate Unique IDs
useId generates unique IDs on both the client and server while avoiding hydration mismatches. It's primarily used for component libraries that integrate with accessibility APIs requiring unique IDs.
import { useId } from 'react';
function FormField({ label }) {
const id = useId(); // Each call generates a unique ID
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
9. Custom Hooks: Encapsulating Reusable Logic
Custom Hooks let you extract component logic into reusable functions. They are React's core mechanism for achieving logic reuse.
Core Rules
- Hook names must start with
use, followed by an uppercase letter - Custom Hooks can call other Hooks (built-in Hooks or other custom Hooks)
- Custom Hooks should only be called when the function component renders
Example 1: useOnlineStatus — Monitor Network Status
Suppose you need to track whether the user is online across multiple components:
// useOnlineStatus.js
import { useState, useEffect } from 'react';
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
// Usage
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Offline'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
return (
<button disabled={!isOnline}>
{isOnline ? 'Save' : 'Reconnecting...'}
</button>
);
}
Example 2: useLocalStorage — Sync with localStorage
// useLocalStorage.js
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Failed to read localStorage:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Failed to write localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'zh');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="zh">Chinese</option>
<option value="en">English</option>
</select>
</div>
);
}
Example 3: useDebounce — Debounce
// useDebounce.js
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;
}
// Usage
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearch) {
// Only initiate a search request after the user stops typing for 500ms
fetchSearchResults(debouncedSearch);
}
}, [debouncedSearch]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Best Practices for Custom Hooks
- Stay Focused: Each custom Hook should have a single, specific purpose
- Avoid Unnecessary Dependencies: Pay attention to the dependency array to prevent infinite loops
- Make Calling Code More Declarative: Good custom Hooks make calling code clearer by constraining their behavior
- Return the Data You Need: Hooks can return any value — arrays, objects, single values, or functions
- Organize by Domain or Feature: Keep related Hooks together
10. Complete Hook Quick Reference
| Hook | Category | Purpose | Key Point |
|---|---|---|---|
useState |
State | Manage simple state | Returns [state, setState] |
useReducer |
State | Manage complex state logic | Suitable for multiple sub-values or complex updates |
useContext |
Context | Read context | Replaces prop drilling |
useRef |
Ref | Hold values that don't trigger renders | .current property is mutable |
useImperativeHandle |
Ref | Customize exposed ref | Used with forwardRef |
useEffect |
Effect | Handle side effects | Dependency array controls execution timing |
useLayoutEffect |
Effect | Execute side effects synchronously | Runs before browser paint |
useMemo |
Performance | Cache computed results | Avoids repeated computation |
useCallback |
Performance | Cache function references | Prevents unnecessary child re-renders |
useTransition |
Other | Mark non-urgent updates | React 18 new feature |
useDeferredValue |
Other | Defer non-urgent values | React 18 new feature |
useId |
Other | Generate unique IDs | React 18 new feature |
useDebugValue |
Other | Debug custom Hooks | Only displayed in React DevTools |
Conclusion
React Hooks are one of the most important innovations in the React ecosystem. From useState to useEffect, from useMemo to custom Hooks, this API design makes function components incredibly powerful while keeping code concise and maintainable.
Remember three core principles:
- Only call Hooks at the top level — don't put them in loops, conditionals, or nested functions
- Only call Hooks from React functions — function components or custom Hooks
- Custom Hooks must start with
use
When you find yourself repeating the same logic across multiple components, it's time to extract a custom Hook. That's exactly what Hooks were designed for — making logic reuse simple and elegant.
If you found this useful, feel free to like, bookmark, and share so more people can benefit! See you next time 👋