React Hooks Cheat Sheet
Quick reference for every React Hook with import statements, syntax, usage tips, and gotchas.
useState
Import: import { useState } from 'react';
When to use: Add local state to a function component for values that trigger re-renders when changed.
Basic syntax
const [count, setCount] = useState(0);
// Direct update
setCount(5);
// Update button
<button onClick={() => setCount(count + 1)}>Increment</button>
Updater function (previous state)
// Safe when multiple updates may be batched
setCount(prev => prev + 1);
// Object state — must spread to merge
const [user, setUser] = useState({ name: '', age: 0 });
setUser(prev => ({ ...prev, name: 'Alice' }));
// Array state
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]);
setItems(prev => prev.filter(item => item.id !== id));
Lazy initialization
// Function runs only on first render — avoids expensive recomputation
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('data')) || [];
});
Gotchas: State updates are asynchronous and batched. Always use the updater function form when the new state depends on the previous state. useState does not merge objects like this.setState did in class components.
useEffect
Import: import { useEffect } from 'react';
When to use: Run side effects after render, such as data fetching, subscriptions, or DOM manipulation.
With dependency array
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Re-runs only when count changes
Cleanup function
useEffect(() => {
const handler = () => console.log('resized');
window.addEventListener('resize', handler);
// Cleanup runs before next effect and on unmount
return () => window.removeEventListener('resize', handler);
}, []);
Empty dependency array (run once on mount)
useEffect(() => {
fetchInitialData();
}, []); // Empty array = mount only
No dependency array (run after every render)
useEffect(() => {
console.log('Runs after every render');
}); // No array = every render
Async inside useEffect
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
const res = await fetch('/api/data', { signal: controller.signal });
const json = await res.json();
setData(json);
}
fetchData();
return () => controller.abort();
}, []);
Gotchas: The effect callback cannot be async directly; define an async function inside it. In React 18+ Strict Mode, effects fire twice in development. Always include all values read inside the effect in the dependency array.
useContext
Import: import { useContext, createContext } from 'react';
When to use: Share values (theme, auth, locale) across the component tree without prop drilling.
Creating and providing context
// Create context with default value
const ThemeContext = createContext('light');
// Provider wraps the tree
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
Consuming context
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button className={theme}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
);
}
Custom hook for context (recommended)
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Gotchas: Every component that calls useContext(Ctx) re-renders when the provider value changes. Wrap the value in useMemo to avoid unnecessary re-renders. The default value is only used when there is no matching Provider above in the tree.
useRef
Import: import { useRef } from 'react';
When to use: Access DOM elements directly or store mutable values that persist across renders without triggering re-renders.
DOM ref
function TextInput() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</>
);
}
Mutable value (no re-render)
function Timer() {
const intervalRef = useRef(null);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1; // Does not trigger re-render
});
function startTimer() {
intervalRef.current = setInterval(() => console.log('tick'), 1000);
}
function stopTimer() {
clearInterval(intervalRef.current);
}
return <>...</>;
}
Gotchas: Changing .current does not cause a re-render. Do not read or write ref.current during rendering (except for lazy initialization). Use forwardRef to pass a ref to a child component.
useMemo
Import: import { useMemo } from 'react';
When to use: Cache the result of an expensive computation so it only recalculates when dependencies change.
Memoizing computed values
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
const total = useMemo(() => {
return cart.reduce((sum, item) => sum + item.price * item.qty, 0);
}, [cart]);
// Memoize object to preserve referential equality
const style = useMemo(() => ({
color: darkMode ? '#fff' : '#000',
background: darkMode ? '#333' : '#fff',
}), [darkMode]);
Gotchas: Do not use useMemo for every value; it adds overhead. Only memoize genuinely expensive computations or values passed as props to memoized children. React may discard cached values — treat useMemo as a performance hint, not a semantic guarantee.
useCallback
Import: import { useCallback } from 'react';
When to use: Cache a function definition between re-renders so child components wrapped in React.memo don't re-render needlessly.
Memoizing functions
const handleSubmit = useCallback((e) => {
e.preventDefault();
submitForm(formData);
}, [formData]);
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
// Pass stable callback to memoized child
<MemoizedList items={items} onDelete={handleDelete} />
useCallback vs useMemo
// These are equivalent:
const memoizedFn = useCallback(fn, [deps]);
const memoizedFn = useMemo(() => fn, [deps]);
Gotchas: useCallback is only useful when passing the function as a prop to an optimized child (e.g., React.memo) or as a dependency of another hook. Wrapping every function in useCallback without a consumer that benefits from it adds overhead for no gain.
useReducer
Import: import { useReducer } from 'react';
When to use: Manage complex state with multiple sub-values or when next state depends on the previous state through explicit actions.
Complex state with dispatch
const initialState = { count: 0, error: null, loading: false };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return initialState;
case 'setError':
return { ...state, error: action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
Lazy initialization
function init(initialCount) {
return { count: initialCount };
}
const [state, dispatch] = useReducer(reducer, 0, init);
Gotchas: The dispatch function identity is stable across renders, making it safe to pass down without useCallback. If the reducer returns the same reference as current state, React bails out of the re-render.
useLayoutEffect
Import: import { useLayoutEffect } from 'react';
When to use: Read layout from the DOM and synchronously re-render before the browser paints, preventing visual flicker.
function Tooltip({ targetRef, children }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}, [targetRef]);
return (
<div ref={tooltipRef} style={{ position: 'absolute', ...position }}>
{children}
</div>
);
}
Gotchas: useLayoutEffect blocks the browser paint, so keep it fast. Prefer useEffect for most side effects. On the server (SSR), useLayoutEffect logs a warning; use useEffect or conditionally check for the browser environment.
Custom Hooks
When to use: Extract reusable stateful logic into a function whose name starts with use.
Pattern
// Custom hooks compose built-in hooks
// Name must start with "use"
// Can call other hooks inside
function useMyHook(param) {
const [state, setState] = useState(initialValue);
useEffect(() => {
// side effect logic
}, [param]);
return state; // or [state, setter], or an object
}
useLocalStorage example
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Usage
const [name, setName] = useLocalStorage('username', 'Guest');
Gotchas: Custom hooks share logic, not state. Each call to a custom hook gets its own isolated state. Follow the Rules of Hooks inside custom hooks just as you would in components.
Rules of Hooks
These rules must be followed for hooks to work correctly.
| Rule | Correct | Incorrect |
|---|---|---|
| Call at the top level only | const [v, setV] = useState(0); |
if (cond) { useState(0); } |
| No hooks in loops | One useState per value |
for (let i...) { useState(i); } |
| No hooks in nested functions | Call in component body | function inner() { useEffect(...) } |
| Only in React functions | Function components, custom hooks | Regular JS functions, classes |
Custom hooks start with use |
function useAuth() {} |
function getAuth() {} |
| Same order every render | Hooks in fixed sequence | Hooks after early return |
Install eslint-plugin-react-hooks to enforce these rules automatically.
Common Patterns
Fetch data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
Debounce
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
const debouncedSearch = useDebounce(searchTerm, 500);
Previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current; // Returns value from previous render
}
// Usage
const prevCount = usePrevious(count);
Toggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
// Usage
const [isOpen, toggleOpen] = useToggle();
const [isDark, toggleDark] = useToggle(true);
Frequently Asked Questions
What is the difference between useMemo and useCallback?
useMemo returns a memoized value, while useCallback returns a memoized function. Use useMemo(() => computeValue(a, b), [a, b]) to cache the result of an expensive computation. Use useCallback((x) => doSomething(x, a), [a]) to cache the function definition itself. In practice, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). Use useCallback when passing callbacks to children wrapped in React.memo.
When should I use useReducer instead of useState?
Use useReducer when your state logic is complex, involves multiple sub-values, or when the next state depends on the previous one in non-trivial ways. It is also preferable when you need to pass dispatch down to deeply nested components, since the dispatch identity is stable across renders. For simple state like a boolean toggle or a single counter, useState is sufficient.
Why does my useEffect run twice in development?
In React 18+ with Strict Mode enabled, React intentionally mounts, unmounts, and remounts components during development to help you find bugs related to missing cleanup functions. This double invocation only happens in development mode and does not occur in production builds. Make sure your effects have proper cleanup functions to handle this behavior correctly.
What are the Rules of Hooks?
There are two main rules: (1) Only call hooks at the top level of your component or custom hook — never inside loops, conditions, or nested functions. (2) Only call hooks from React function components or custom hooks — never from regular JavaScript functions. These rules ensure React can correctly track hook state between renders. Install eslint-plugin-react-hooks to enforce these rules automatically.