React Hooks are easy to learn individually. The harder part is knowing which Hook to use, when to use it, and when you probably do not need a Hook at all.
I have seen React components become unnecessarily complicated because every small problem was solved with useEffect, useMemo, or useCallback. In real projects, simpler code is usually easier to maintain.
This React Hooks cheat sheet focuses on practical usage rather than memorizing APIs. We will cover the Hooks I use most often, common mistakes, performance Hooks, modern form and UI Hooks, and a few advanced Hooks that you may only need occasionally.
React Hooks Quick Reference
| Hook | Use it for |
|---|---|
useState |
Simple local component state |
useEffect |
Synchronizing with external systems |
useContext |
Reading shared context values |
useReducer |
Complex state transitions |
useRef |
DOM references and mutable values |
useMemo |
Caching expensive calculations |
useCallback |
Caching function references |
useTransition |
Marking UI updates as non-blocking |
useDeferredValue |
Deferring slower UI updates |
useId |
Generating IDs for accessibility |
useActionState |
Managing state around Actions |
useOptimistic |
Showing optimistic UI updates |
useEffectEvent |
Reading latest values inside Effects without re-running them |
useFormStatus |
Reading pending form submission state |
useLayoutEffect |
Layout measurement before paint |
useImperativeHandle |
Controlling what a ref exposes |
useDebugValue |
Debug labels for custom Hooks |
useSyncExternalStore |
Subscribing to external stores |
What Are React Hooks?
Hooks are functions that let React components use features such as state, context, refs, Effects, and transitions.
A simple Hook call looks like this:
const [count, setCount] = useState(0);
The benefit is not simply that Hooks replaced class components. The bigger advantage is that reusable logic can be extracted into small functions called custom Hooks instead of being tied to lifecycle methods.
The Rules of Hooks
Most React Hooks follow two important rules.
- Call Hooks at the top level of your component or custom Hook.
- Call Hooks only from React components or other Hooks.
Do not normally do this:
if (loggedIn) {
const [user, setUser] = useState(null);
}
React depends on Hook calls happening in a consistent order between renders.
1. useState: Local Component State
useState is usually the first Hook you learn and still one of the most useful.
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Use it for small pieces of state such as:
- modal visibility
- selected tabs
- form field values
- filters
- counters
- loading flags
Prefer functional updates when state depends on previous state
setCount((previousCount) => previousCount + 1);
I prefer this form whenever the next value depends on the old value. It avoids subtle stale-state problems when React batches updates.
2. useEffect: Synchronize with Something Outside React
useEffect is probably the most misused React Hook.
I try to think of it this way:
If my component needs to synchronize with something outside React, an Effect may be appropriate.
Typical examples include:
- browser APIs
- WebSocket connections
- timers
- third-party widgets
- event listeners
- network synchronization
import { useEffect } from 'react';
function OnlineStatus() {
useEffect(() => {
function handleOnline() {
console.log('Browser is online');
}
window.addEventListener('online', handleOnline);
return () => {
window.removeEventListener('online', handleOnline);
};
}, []);
return null;
}
A common mistake
You often do not need an Effect just to calculate something from existing state.
Avoid:
const [firstName, setFirstName] = useState('Jay');
const [lastName, setLastName] = useState('Patel');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Prefer:
const fullName = `${firstName} ${lastName}`;
That is simpler and avoids an unnecessary render.
3. useContext: Share Values Without Prop Drilling
useContext is useful when many components need the same value.
A common example is application theme state.
import {
createContext,
useContext,
} from 'react';
const ThemeContext = createContext('light');
function Toolbar() {
const theme = useContext(ThemeContext);
return <p>Current theme: {theme}</p>;
}
export default function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
Good uses include:
- authentication information
- theme preferences
- locale settings
- feature configuration
I would not put every piece of application state into Context. Frequently changing large Context values can cause unnecessary re-renders.
4. useReducer: When State Updates Become Complex
Once several state values change together based on defined actions, useReducer can be easier to reason about than many separate useState calls.
import { useReducer } from 'react';
const initialState = {
count: 0,
};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
};
case 'decrement':
return {
...state,
count: state.count - 1,
};
default:
return state;
}
}
export default function Counter() {
const [state, dispatch] = useReducer(
reducer,
initialState
);
return (
<div>
<p>{state.count}</p>
<button
onClick={() => dispatch({ type: 'increment' })}
>
+
</button>
<button
onClick={() => dispatch({ type: 'decrement' })}
>
-
</button>
</div>
);
}
I tend to reach for useReducer when a feature starts having multiple related transitions such as:
- shopping carts
- multi-step forms
- complex filters
- editor state
- workflow state
5. useRef: DOM References and Values That Should Not Trigger Rendering
useRef has two common uses.
Access a DOM element
import { useRef } from 'react';
export default function SearchBox() {
const inputRef = useRef(null);
function focusSearch() {
inputRef.current?.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={focusSearch}>
Focus search
</button>
</>
);
}
Store a mutable value without causing a render
const requestCount = useRef(0);
requestCount.current += 1;
Changing requestCount.current does not trigger a component render. That is exactly what you want for some internal values, but not for UI state that needs to appear on screen.
6. useMemo: Cache an Expensive Calculation
useMemo can cache the result of a calculation between renders.
import { useMemo } from 'react';
function ProductList({ products, search }) {
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name
.toLowerCase()
.includes(search.toLowerCase())
);
}, [products, search]);
return (
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
{product.name}
</li>
))}
</ul>
);
}
One thing I would avoid is adding useMemo everywhere because a calculation looks slightly complex.
Use it when you have an actual performance reason, such as an expensive calculation or a value whose stable identity matters to another optimized component.
7. useCallback: Keep a Function Reference Stable
useCallback is similar to useMemo, except it caches a function definition.
import {
memo,
useCallback,
useState,
} from 'react';
const SaveButton = memo(function SaveButton({
onSave,
}) {
console.log('SaveButton rendered');
return (
<button onClick={onSave}>
Save
</button>
);
});
export default function Editor() {
const [text, setText] = useState('');
const handleSave = useCallback(() => {
console.log('Saving document');
}, []);
return (
<>
<input
value={text}
onChange={(event) =>
setText(event.target.value)
}
/>
<SaveButton onSave={handleSave} />
</>
);
}
The important detail is that useCallback does not magically make a function faster. It keeps its reference stable between renders when dependencies have not changed.
8. useTransition: Keep Expensive UI Updates Responsive
useTransition helps when one update should happen immediately while another potentially expensive UI update can wait.
Search filtering is a good example.
import {
useState,
useTransition,
} from 'react';
export default function SearchPage() {
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('');
const [isPending, startTransition] =
useTransition();
function handleChange(event) {
const value = event.target.value;
setQuery(value);
startTransition(() => {
setFilter(value);
});
}
return (
<>
<input
value={query}
onChange={handleChange}
/>
{isPending && <p>Updating results...</p>}
<SearchResults query={filter} />
</>
);
}
The input update stays urgent, while the expensive results update is treated as non-blocking.
9. useDeferredValue: Let a Slower Part of the UI Catch Up
useDeferredValue is useful when you already have a value but want a slower part of the UI to update later.
import {
useDeferredValue,
useState,
} from 'react';
export default function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery =
useDeferredValue(query);
return (
<>
<input
value={query}
onChange={(event) =>
setQuery(event.target.value)
}
/>
<SearchResults
query={deferredQuery}
/>
</>
);
}
I usually think of the difference this way:
useTransitioncontrols how you perform an update.useDeferredValuegives you a deferred version of an existing value.
10. useId: Generate Accessible IDs
useId is useful when related form elements need unique IDs.
import { useId } from 'react';
export default function EmailField() {
const emailId = useId();
return (
<div>
<label htmlFor={emailId}>
Email address
</label>
<input
id={emailId}
type="email"
/>
</div>
);
}
Do not use useId to generate keys for list items. List keys should come from the data itself.
11. useActionState: Manage State Around an Action
useActionState is useful for workflows where an Action produces a new state, especially forms and async operations.
import { useActionState } from 'react';
async function saveProfile(
previousState,
formData
) {
const name = formData.get('name');
if (!name) {
return {
success: false,
message: 'Name is required',
};
}
await updateProfile({ name });
return {
success: true,
message: 'Profile updated',
};
}
export default function ProfileForm() {
const [state, action, isPending] =
useActionState(saveProfile, {
success: false,
message: '',
});
return (
<form action={action}>
<input
name="name"
placeholder="Name"
/>
<button disabled={isPending}>
{isPending
? 'Saving...'
: 'Save profile'}
</button>
{state.message && (
<p>{state.message}</p>
)}
</form>
);
}
This keeps the result of an Action, the action dispatcher, and its pending state together rather than requiring several separate state variables.
12. useOptimistic: Make Interfaces Feel Faster
An optimistic interface updates immediately before a network request finishes, then confirms or rolls back depending on the result.
This can make comments, likes, cart updates, and similar interactions feel significantly faster.
import { useOptimistic } from 'react';
function CommentList({ comments, addComment }) {
const [
optimisticComments,
addOptimisticComment,
] = useOptimistic(
comments,
(currentComments, newComment) => [
...currentComments,
{
...newComment,
pending: true,
},
]
);
async function submitComment(formData) {
const text = formData.get('comment');
const optimisticComment = {
id: crypto.randomUUID(),
text,
};
addOptimisticComment(
optimisticComment
);
await addComment(text);
}
return (
<>
<form action={submitComment}>
<input
name="comment"
placeholder="Write a comment"
/>
<button>Post</button>
</form>
{optimisticComments.map(
(comment) => (
<p key={comment.id}>
{comment.text}
{comment.pending
? ' (sending...)'
: ''}
</p>
)
)}
</>
);
}
The UI does not have to wait for the server before showing the new item.
13. useFormStatus: Show Form Submission State
useFormStatus comes from react-dom and can read the status of a parent form submission.
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } =
useFormStatus();
return (
<button
type="submit"
disabled={pending}
>
{pending
? 'Submitting...'
: 'Submit'}
</button>
);
}
export default function ContactForm({
action,
}) {
return (
<form action={action}>
<input name="email" />
<SubmitButton />
</form>
);
}
One important detail: the component calling useFormStatus needs to be rendered inside the form it is tracking.
14. useEffectEvent: Read Latest Values Without Reconnecting an Effect
useEffectEvent solves a subtle problem that appears when an Effect needs access to the latest state but should not re-run every time that state changes.
import {
useEffect,
useEffectEvent,
} from 'react';
function ChatRoom({
roomId,
theme,
}) {
const onConnected =
useEffectEvent(() => {
showNotification(
'Connected',
theme
);
});
useEffect(() => {
const connection =
createConnection(roomId);
connection.on(
'connected',
onConnected
);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return null;
}
The connection depends on roomId, but changing the visual theme should not force the connection to restart.
I would not use this Hook simply to avoid fixing dependency warnings. It is for logic that genuinely belongs to an Effect but should remain non-reactive.
15. useLayoutEffect: Measure Layout Before the Browser Paints
useLayoutEffect runs earlier than a normal Effect and is mainly useful when you must measure or adjust layout before the user sees the frame.
import {
useLayoutEffect,
useRef,
useState,
} from 'react';
function Tooltip() {
const tooltipRef = useRef(null);
const [height, setHeight] =
useState(0);
useLayoutEffect(() => {
const rect =
tooltipRef.current
?.getBoundingClientRect();
setHeight(rect?.height ?? 0);
}, []);
return (
<div ref={tooltipRef}>
Tooltip height: {height}
</div>
);
}
I use this rarely. If useEffect works, prefer it because layout Effects can block browser painting.
16. useImperativeHandle: Control What a Ref Exposes
Sometimes a parent needs a very small imperative API from a child component.
import {
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
const SearchInput = forwardRef(
function SearchInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(
ref,
() => ({
focus() {
inputRef.current?.focus();
},
}),
[]
);
return (
<input
ref={inputRef}
type="search"
/>
);
}
);
I would keep this as a last-resort integration tool rather than a normal state-management pattern.
17. useSyncExternalStore: Subscribe to External State
useSyncExternalStore is useful when React needs to subscribe safely to state managed outside React.
A simple browser example is online status:
import {
useSyncExternalStore,
} from 'react';
function subscribe(callback) {
window.addEventListener(
'online',
callback
);
window.addEventListener(
'offline',
callback
);
return () => {
window.removeEventListener(
'online',
callback
);
window.removeEventListener(
'offline',
callback
);
};
}
function getSnapshot() {
return navigator.onLine;
}
export default function Status() {
const isOnline =
useSyncExternalStore(
subscribe,
getSnapshot
);
return (
<p>
{isOnline
? 'Online'
: 'Offline'}
</p>
);
}
This Hook is particularly important for library authors and integrations with external stores.
18. useDebugValue: Improve Custom Hook Debugging
useDebugValue lets a custom Hook display a more useful label in React DevTools.
import {
useDebugValue,
useSyncExternalStore,
} from 'react';
function useOnlineStatus() {
const isOnline =
useSyncExternalStore(
subscribe,
getSnapshot
);
useDebugValue(
isOnline
? 'Online'
: 'Offline'
);
return isOnline;
}
You probably will not need this in everyday application components, but it becomes helpful when building reusable custom Hooks.
Custom Hooks: Where Hooks Become Really Powerful
The part of Hooks I find most valuable in real projects is extracting reusable behavior.
For example, instead of repeating online-status logic in several components:
import {
useSyncExternalStore,
} from 'react';
function subscribe(callback) {
window.addEventListener(
'online',
callback
);
window.addEventListener(
'offline',
callback
);
return () => {
window.removeEventListener(
'online',
callback
);
window.removeEventListener(
'offline',
callback
);
};
}
function getSnapshot() {
return navigator.onLine;
}
export function useOnlineStatus() {
return useSyncExternalStore(
subscribe,
getSnapshot
);
}
Then your UI becomes very simple:
function Header() {
const isOnline =
useOnlineStatus();
return (
<p>
{isOnline
? 'Connected'
: 'No connection'}
</p>
);
}
A custom Hook should usually represent a meaningful reusable behavior rather than simply wrapping one line of React state.
A Hook I Do Not Use Unless I Need It: useInsertionEffect
useInsertionEffect exists primarily for libraries that need to inject styles before React performs layout Effects.
If you are building normal application features, you probably do not need it.
This is an important lesson with React Hooks: knowing an API exists does not mean it belongs in every project.
What About the React use API?
Modern React also includes an API named use. Despite the name, React documents it separately from normal Hooks.
It can read resources such as Promises or Context values.
import { use } from 'react';
function UserName({
userPromise,
}) {
const user =
use(userPromise);
return (
<p>{user.name}</p>
);
}
When used with a Promise and Suspense, rendering can pause until the resource is ready.
One unusual difference is that use can be called conditionally in situations where normal Hooks cannot.
Common React Hooks Mistakes I See in Real Projects
1. Using useEffect for derived values
If a value can be calculated directly from props or state, calculate it while rendering instead of storing another copy in state.
2. Using useMemo everywhere
Memoization itself has a cost. Measure first, optimize second.
3. Using useCallback without a reason
If the function is not passed to an optimized child and its identity is not important, a normal function may be simpler.
4. Missing Effect cleanup
Subscriptions, listeners, timers, and connections usually need cleanup.
useEffect(() => {
const timer =
setInterval(refresh, 5000);
return () =>
clearInterval(timer);
}, []);
5. Ignoring dependency warnings
Removing a dependency simply to stop the linter warning often creates stale state bugs.
Understand why the Effect is re-running instead of hiding the warning.
6. Putting everything into Context
Context is useful, but a huge frequently changing Context can cause broad re-renders and make dependencies difficult to understand.
7. Creating giant custom Hooks
A custom Hook that contains authentication, analytics, API calls, local storage, navigation, notifications, and form logic is not necessarily better than a large component.
Keep responsibilities focused.
Which Hook Should I Use?
This is the decision process I generally follow.
Need local UI state?
→ useState
Complex state transitions?
→ useReducer
Need shared context?
→ useContext
Need DOM access or mutable storage?
→ useRef
Synchronizing with external system?
→ useEffect
Expensive calculation?
→ consider useMemo
Stable callback needed?
→ consider useCallback
Slow UI update?
→ useTransition / useDeferredValue
Form Action state?
→ useActionState
Optimistic UX?
→ useOptimistic
Form pending status?
→ useFormStatus
Need latest values inside an Effect?
→ useEffectEvent
External store subscription?
→ useSyncExternalStore
What I Use Most Often in Production
Although React provides many Hooks, I do not use all of them equally.
In normal application code, most of my components are built primarily with:
useStateuseEffectuseRefuseContext- custom Hooks
I bring in useMemo, useCallback, transitions, reducers, and advanced Hooks when the problem actually requires them.
That approach has generally produced cleaner React code than trying to optimize every component from the beginning.
Final Thoughts
The best React developers are not the ones who use the most Hooks. They are the ones who know when a Hook makes the component easier to understand and when plain JavaScript is enough.
If you are learning Hooks, start with useState, useEffect, useRef, and useContext. Build a few real components first.
Then learn useReducer, performance Hooks, transitions, Actions, optimistic updates, and the more specialized APIs as your applications become more complex.
The goal is not to memorize every React API. The goal is to understand the problem each Hook solves so that when you encounter that problem in a real project, you know which tool fits naturally.




