Welcome Back, React Hooks Made Simple: The Complete Guide, Part-2
In [Part 1] https://dev.to/abrar_galib_5c0cf41ad3a3e/react-hooks-made-simple-the-complete-guide-part-1-2jln we covered the core Hooks (useState, useEffect, useContext), the additional Hooks (useRef, useMemo, useCallback, useReducer) and the advanced ones (useLayoutEffect, useImperativeHandle, useId). If you haven't read it yet, start there, because everything in this part builds on it.
In Part 2:
* Two Hooks that keep your app smooth when an update is heavy
* A few more advanced Hooks for outside data and library authors
* The new Hooks that came with React 19 and later
* The Hooks that belong to Next.js
* Custom Hooks, common mistakes, and a cheatsheet you can keep open while you code
Everything below works on React 19. A few things need a newer minor version, and I point them out as we go: `useEffectEvent` needs React 19.2, and `ViewTransition`, Fragment Refs and `use(browser())` need React 19.3, which is the current stable release.
## **Table of Contents**
* Performance Hooks
* useTransition
* useDeferredValue
* More Advanced Hooks
* useSyncExternalStore
* useInsertionEffect
* useDebugValue
* New Hooks in React 19 and Later
* useActionState
* useFormStatus
* useOptimistic
* use
* useEffectEvent
* Bonus: ViewTransition and Fragment Refs
* Next.js Hooks
* useRouter
* usePathname
* useSearchParams
* useParams
* useLinkStatus
* useServerInsertedHTML
* Custom Hooks
* Common Mistakes to Avoid
* Cheatsheet
* Conclusion
## **Performance Hooks**
Sometimes an update is heavy: a huge list, a slow tab, a big search result. If React does all the work at once, the page freezes. Typing feels laggy and buttons stop responding.
These two Hooks let you tell React: "this update is not urgent, so do the important things first."
**What is an urgent update?**
Anything the user expects to see instantly, like typing in a box or clicking a button. Showing the search results or opening a heavy tab can wait a moment. React can handle the urgent update first and finish the slow one in the background.
### useTransition
useTransition lets you mark a state update as low priority. It gives you two things: `isPending`, which is `true` while the slow update is happening, and `startTransition`, a function you wrap around the state update.
'use client';
import { useState, useTransition } from 'react';
function About() {
return <p>Hi, I write about React.</p>;
}
function SlowPosts() {
// Pretend this tab is heavy: 300 rows, and each row takes a little time
const rows = [];
for (let i = 0; i < 300; i++) {
rows.push(<SlowRow key={i} number={i + 1} />);
}
return <ul>{rows}</ul>;
}
function SlowRow({ number }) {
const start = performance.now();
while (performance.now() - start < 2) {
// do nothing for 2 ms on purpose
}
return <li>Post #{number}</li>;
}
export default function TabContainer() {
const [tab, setTab] = useState('about');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab) {
// This update can wait, so the page stays clickable
startTransition(() => {
setTab(nextTab);
});
}
return (
<>
<button onClick={() => selectTab('about')}>About</button>
<button onClick={() => selectTab('posts')}>Posts</button>
{isPending && <p>Loading...</p>}
{tab === 'about' ? <About /> : <SlowPosts />}
</>
);
}
Click "Posts" and the old tab stays on screen while React prepares the new one in the background. You can click "About" again at any time and React will drop the slow work and switch back. Without `startTransition`, the whole page would freeze for a moment.
**Note:** In React 19 you can even pass an `async` function to `startTransition`, and `isPending` stays `true` until it finishes. This is the base for the form Hooks later in this guide.
### useDeferredValue
useDeferredValue gives you a copy of a value that intentionally "lags behind". While the user keeps typing, your input stays fast, and the heavy part of the screen catches up a moment later.
Use it when you can't wrap the state setter yourself, for example when the value arrives as a prop.
'use client';
import { memo, useDeferredValue, useState } from 'react';
const Results = memo(function Results({ items, query }) {
const matches = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<ul>
{matches.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
});
export default function Search({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Results items={items} query={deferredQuery} />
</div>
</>
);
}
Two things to remember:
* You must wrap the slow component in `memo`. If you don't, it re-renders on every keystroke anyway and you save nothing.
* `query !== deferredQuery` tells you the list is still catching up. Here we use it to dim the old results.
React 19 also lets you pass a starting value as the second argument, like `useDeferredValue(query, '')`.
**useTransition or useDeferredValue?**
| useTransition | useDeferredValue
---|---|---
What it wraps | The code that sets state | The value itself
Use it when | You own the state setter | The value comes from a prop or a Hook
You get | `isPending` flag | A lagging copy of the value
**Note:** Don't wrap the `setState` of a text input in `startTransition`. The text in the box must update instantly, so the input state has to stay urgent. Defer the slow part instead, like we did above.
## **More Advanced Hooks**
You will use these three much less often than the ones above. Still, you should know what they are, because you'll see them in libraries and in other people's code.
### useSyncExternalStore
useSyncExternalStore connects React to data that lives outside of React and changes on its own, like the browser's online status, the window size, or a global store.
**What is an external store?**
It's any data source that React doesn't own. React can't know when it changes, so you have to tell React how to listen to it. Libraries like Redux and Zustand use this Hook under the hood.
It takes three functions:
* `subscribe`: starts listening for changes and returns a cleanup function
* `getSnapshot`: reads the current value
* `getServerSnapshot`: gives a starting value for server rendering, so the server HTML matches the browser HTML during hydration
'use client';
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 useOnlineStatus() {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // getSnapshot: read the browser value
() => true // getServerSnapshot: what the server should assume
);
}
export default function StatusBar() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? 'Online' : 'Offline'}</p>;
}
Why not just use `useEffect` and `useState`? Because this Hook also prevents a bug called tearing, where two parts of the screen show different values of the same data during a render.
Two rules to remember:
* Define `subscribe` outside your component. If it's created inside, React will unsubscribe and subscribe again on every render.
* `getSnapshot` must return the same value when nothing changed. If it returns a brand-new object every time, you will get an infinite loop.
In Next.js, `getServerSnapshot` is required if the component renders on the server.
### useInsertionEffect
This one is built for people who write CSS-in-JS libraries. It runs before React changes the DOM, so a library can safely inject `<style>` tags at the right moment. If you build normal apps, you will probably never need it.
Here is the order in which the three effect Hooks run:
1. `useInsertionEffect` runs before React updates the DOM
2. `useLayoutEffect` runs after the DOM updates, before the browser paints
3. `useEffect` runs after the browser paints
'use client';
import { useInsertionEffect } from 'react';
export default function Highlight({ children }) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = '.highlight { background: yellow; }';
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, []);
return <mark className="highlight">{children}</mark>;
}
Libraries like styled-components and Emotion already do this for you, so just use them.
### useDebugValue
A tiny Hook that adds a label next to your custom Hook inside React DevTools. It only helps with debugging, and it only works inside custom Hooks.
import { useDebugValue, useSyncExternalStore } from 'react';
// "subscribe" is the same function from the previous example
function useOnlineStatus() {
const isOnline = useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
Open DevTools, find the component that uses `useOnlineStatus`, and you will see the `Online` or `Offline` label right next to it.
## **New Hooks in React 19 and Later**
React 19 was built around one idea: **Actions**. A few new Hooks make Actions easy to use, especially with forms. Then React 19.2 and 19.3 added a few more useful things on top.
**What is an Action?**
An Action is a function that changes something, usually by talking to a server. Submitting a form is the classic example. React runs it inside a transition, so it can track a pending state for you, and it updates the screen when the work is done.
### useActionState
useActionState runs a function when a form is submitted and keeps track of the result. You get the latest result, a function to put on your form, and a `isPending` flag.
'use client';
import { useActionState } from 'react';
async function subscribe(previousState, formData) {
const email = formData.get('email');
// Pretend we are talking to a server
await new Promise((resolve) => setTimeout(resolve, 1000));
if (!email.includes('@')) {
return { message: 'Please enter a valid email.' };
}
return { message: `Thanks! We will write to ${email}.` };
}
export default function NewsletterForm() {
const [state, formAction, isPending] = useActionState(subscribe, {
message: '',
});
return (
<form action={formAction}>
<input name="email" type="text" placeholder="you@example.com" />
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Subscribe'}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}
How it works:
1. You pass in your function and a starting state.
2. You put `formAction` on the form's `action` prop.
3. When the form is submitted, React calls your function with the previous state and the form data.
4. Whatever your function returns becomes the new `state`.
Notice that we don't need `onSubmit`, `preventDefault` or a `useState` for every input. React reads the form data for us.
**Note:** After the action finishes, React resets the form's fields. That's normal behavior in React 19.
In Next.js you can pass a Server Action instead. Put it in its own file with `'use server'` at the top, then import it:
// app/actions.js
'use server';
export async function subscribe(previousState, formData) {
// save to your database here
return { message: 'Thanks for subscribing!' };
}
You might see the old name `useFormState` in older tutorials. It was renamed to `useActionState`.
### useFormStatus
useFormStatus tells a component whether the form it sits inside is currently submitting. It's perfect for a submit button. You import it from `react-dom`, not from `react`.
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
async function saveProfile(formData) {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Saved:', formData.get('name'));
}
export default function ProfileForm() {
return (
<form action={saveProfile}>
<input name="name" placeholder="Your name" />
<SubmitButton />
</form>
);
}
**Note:** `useFormStatus` only works in a component that is rendered _inside_ a `<form>`. If you call it in the same component that renders the `<form>`, `pending` will always be `false`. That's why the button is its own component here. The nice part is that you can reuse `SubmitButton` in every form in your app.
### useOptimistic
**What is Optimistic UI?**
When you like a post on a social app, the heart turns red right away. It doesn't wait for the server. The app is being optimistic: it assumes the server will say yes, shows the result immediately, and fixes things later if something goes wrong.
useOptimistic gives you a temporary value that lasts only while an Action is running. When the Action finishes, React switches back to the real state.
'use client';
import { useOptimistic, useState, useTransition } from 'react';
async function saveLike() {
// Pretend this is a slow server call
await new Promise((resolve) => setTimeout(resolve, 1500));
}
export default function LikeButton() {
const [likes, setLikes] = useState(0);
const [, startTransition] = useTransition();
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(currentLikes, amount) => currentLikes + amount
);
function handleClick() {
startTransition(async () => {
addOptimisticLike(1); // the screen updates instantly
await saveLike(); // the real work takes 1.5 seconds
setLikes((current) => current + 1); // now update the real state
});
}
return <button onClick={handleClick}>Likes: {optimisticLikes}</button>;
}
Click the button and the number goes up right away, even though the "server" takes 1.5 seconds. Once the Action is done, React drops the temporary value and shows the real one, which is the same number, so nothing jumps. If the save fails, the temporary value disappears and the real number is shown again.
The setter (`addOptimisticLike`) must be called inside an Action, like inside `startTransition` or a form `action`. If you call it anywhere else, React will show you a warning.
**Note:** `useOptimistic` is a React Hook. It's not specific to Next.js, even though it works great with Server Actions.
### use
`use` lets you read a resource while rendering. It works with two things: a promise and a context.
It looks like a Hook, but it has a special power: you can call it inside `if` statements and loops, and after an early return. Normal Hooks can't do that. The one place you can't use it is inside `try` / `catch`.
**Reading a promise**
The best pattern is to create the promise in a Server Component, pass it down, and wrap the client component in `Suspense`.
// app/page.js (a Server Component)
import { Suspense } from 'react';
import Comments from './comments';
export default function Page() {
const commentsPromise = fetch(
'https://jsonplaceholder.typicode.com/comments?_limit=5'
).then((res) => res.json());
return (
<Suspense fallback={<p>Loading comments...</p>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}
// app/comments.js
'use client';
import { use } from 'react';
export default function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // waits here until the data arrives
return (
<ul>
{comments.map((comment) => (
<li key={comment.id}>{comment.name}</li>
))}
</ul>
);
}
While the promise is loading, React shows the `Suspense` fallback. When it's ready, the component renders with the data. There is no `useEffect`, no loading state and no `isLoading` flag.
**Note:** Don't create a new promise inside a Client Component while rendering, and then pass it to `use`. It would create a fresh promise on every render. Create it in a Server Component, or use a library that caches it.
**Reading context, even after an`if`**
import { use } from 'react';
import { ThemeContext } from './theme-context';
function Heading({ children }) {
if (children == null) {
return null;
}
// useContext can't be called here, but use can
const theme = use(ThemeContext);
return <h1 className={theme}>{children}</h1>;
}
**New in React 19.3:`use(browser())`**
Some components can't make sensible HTML on the server, for example because they read the visitor's time zone. Calling `use(browser())` tells React to skip this component during server rendering and show the nearest `Suspense` fallback instead. Once the page loads in the browser, it renders normally.
import { Suspense, use } from 'react';
import { browser } from 'react-dom';
function TimeZone() {
use(browser());
const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
return <p>{timeZone}</p>;
}
export default function App() {
return (
<Suspense fallback="Loading...">
<TimeZone />
</Suspense>
);
}
Before 19.3, people did this with a `mounted` state and a `useEffect`. Now there is a proper way.
**Note:** In recent Next.js versions, the `params` prop of a page is a promise. In a Client Component page you read it with `use`:
'use client';
import { use } from 'react';
export default function Page({ params }) {
const { slug } = use(params);
return <h1>{slug}</h1>;
}
### useEffectEvent
_Needs React 19.2 or newer._
Here is a problem you will meet sooner or later. Your effect needs to _read_ a value, but you don't want the effect to _re-run_ when that value changes.
Imagine we log a page visit, and we also want to log how many items are in the cart:
'use client';
import { useEffect, useEffectEvent } from 'react';
function logVisit(url, numberOfItems) {
console.log(`Visited ${url} with ${numberOfItems} items in the cart`);
}
export default function Page({ url, numberOfItems }) {
// This function always sees the latest numberOfItems
const onVisit = useEffectEvent((visitedUrl) => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // only url here. No numberOfItems, no onVisit.
return <h1>Page: {url}</h1>;
}
If we had put `numberOfItems` in the dependency array, the visit would be logged again every time the cart changes, which is wrong. If we left it out without `useEffectEvent`, the effect would read an old value. `useEffectEvent` solves both problems.
Rules to remember:
* Only call an Effect Event from inside an effect.
* Don't pass it to other components or Hooks.
* Never put it in the dependency array.
**Note:** You might see `useEvent` in older blog posts. That was the early experimental name, and it never shipped in stable React. The real name is `useEffectEvent`.
### Bonus: ViewTransition and Fragment Refs
React 19.3 made two more features stable. Neither of them is a Hook, but you will start seeing them in newer codebases, so here is a quick look.
**ViewTransition**
`<ViewTransition>` animates elements when they enter, exit or change, using the browser's View Transition API. By default you get a smooth cross-fade.
'use client';
import { ViewTransition, useState, startTransition } from 'react';
export default function Toggle() {
const [show, setShow] = useState(false);
return (
<>
<button
onClick={() => {
startTransition(() => {
setShow((prev) => !prev);
});
}}
>
{show ? 'Hide' : 'Show'} card
</button>
{show && (
<ViewTransition>
<div className="card">Hello! I fade in and out.</div>
</ViewTransition>
)}
</>
);
}
React picks the animation based on what happened: **enter** (added), **exit** (removed), **update** (children changed) or **share** (a named one moved from one place to another).
The important rule: only updates marked as a Transition animate. That means state updates inside `startTransition`, a `Suspense` reveal, or an update from `useDeferredValue`. That's why we learned `useTransition` first. It currently works in the DOM only, not in React Native.
**Fragment Refs**
Before, you needed a wrapper `<div>` just to attach a ref to a group of elements. Now you can pass a ref straight to a `<Fragment>` and work with its DOM children as a group.
'use client';
import { Fragment, useRef } from 'react';
export default function Links() {
const fragmentRef = useRef(null);
return (
<>
<button onClick={() => fragmentRef.current.focus()}>
Focus the first link
</button>
<Fragment ref={fragmentRef}>
<a href="#one">One</a>
<a href="#two">Two</a>
</Fragment>
</>
);
}
The ref gives you a `FragmentInstance` with methods for events (`addEventListener`), focus (`focus`, `focusLast`, `blur`), observers (`observeUsing`) and scrolling (`scrollIntoView`). You must write `<Fragment>` in full. The short `<>` syntax can't take a ref.
## **Next.js Hooks**
These Hooks don't come from React. They come from Next.js, and they work in the App Router (the `app` folder). Because they read browser and route information, they only work in **Client Components** , so put `'use client'` at the top of the file.
**Note:** If you see `import { useRouter } from 'next/router'`, that's the old Pages Router. In the App Router, always import from `next/navigation`.
### useRouter
useRouter lets you change pages from your code, for example after a login. For normal links, use the `<Link>` component instead.
'use client';
import { useRouter } from 'next/navigation';
export default function LoginButton() {
const router = useRouter();
async function handleLogin() {
// ...log the user in...
router.push('/dashboard');
}
return <button onClick={handleLogin}>Log in</button>;
}
The router object has these methods:
* `router.push('/path')` goes to a new page and adds it to the history
* `router.replace('/path')` goes to a new page without adding to the history
* `router.back()` and `router.forward()` move through the history
* `router.refresh()` reloads the current route's data
* `router.prefetch('/path')` loads a page in the background before the user clicks
### usePathname
usePathname returns the current URL path, like `/blog/my-post`. A very common use is highlighting the active link in a navbar.
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const links = [
{ href: '/', label: 'Home' },
{ href: '/blog', label: 'Blog' },
];
export default function NavBar() {
const pathname = usePathname();
return (
<nav>
{links.map((link) => (
<Link
key={link.href}
href={link.href}
style={{ fontWeight: pathname === link.href ? 'bold' : 'normal' }}
>
{link.label}
</Link>
))}
</nav>
);
}
### useSearchParams
useSearchParams reads the query string, the part of the URL after the `?`, like `/products?sort=asc`. It's read-only. To change it, build a new query string and navigate to it.
'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export default function SortButtons() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const sort = searchParams.get('sort') ?? 'asc';
function changeSort(value) {
const params = new URLSearchParams(searchParams.toString());
params.set('sort', value);
router.push(`${pathname}?${params.toString()}`);
}
return (
<>
<p>Sorted by: {sort}</p>
<button onClick={() => changeSort('asc')}>Ascending</button>
<button onClick={() => changeSort('desc')}>Descending</button>
</>
);
}
Wrap the component that uses `useSearchParams` in a `Suspense` boundary. On a statically rendered page, the query string isn't known while building, so Next.js needs somewhere to show a fallback:
// app/products/page.js
import { Suspense } from 'react';
import SortButtons from './sort-buttons';
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<SortButtons />
</Suspense>
);
}
Keeping filters and sorting in the URL is a great habit. Users can refresh the page or share the link and see the same result.
### useParams
useParams reads the dynamic parts of the URL. If your file is `app/blog/[slug]/page.js` and the user visits `/blog/react-hooks`, then `slug` is `react-hooks`.
'use client';
import { useParams } from 'next/navigation';
export default function PostTitle() {
const { slug } = useParams();
return <h1>Reading: {slug}</h1>;
}
There is also `useSelectedLayoutSegment`, which tells a layout which child route is currently active. It's handy for tabs and sidebars.
### useLinkStatus
useLinkStatus tells you if a `<Link>` is still loading its page. It's great for showing a small spinner right inside the link, so the user knows the click worked. You import it from `next/link`.
It must be used in a component that is rendered _inside_ the `<Link>`, just like `useFormStatus` must be inside a `<form>`.
'use client';
import Link, { useLinkStatus } from 'next/link';
function Spinner() {
const { pending } = useLinkStatus();
return pending ? <span> ⏳</span> : null;
}
export default function NavLink({ href, children }) {
return (
<Link href={href}>
{children}
<Spinner />
</Link>
);
}
### useServerInsertedHTML
Like `useInsertionEffect`, this one is for library authors, mainly CSS-in-JS libraries. It lets you insert HTML, like a `<style>` tag, into the page while it is being rendered on the server. You import it from `next/navigation`.
'use client';
import { useServerInsertedHTML } from 'next/navigation';
export default function FontStyles() {
useServerInsertedHTML(() => (
<style>{`body { font-family: Arial, sans-serif; }`}</style>
));
return null;
}
If you build normal apps, you will almost never write this yourself.
## **Custom Hooks**
A custom Hook is just a function whose name starts with `use` and that calls other Hooks inside it. That's the whole trick. You take logic that you copied into two components, move it into one function, and use it in both places.
**Note:** Each component that calls a custom Hook gets its own separate state. Two components using `useToggle` don't share the same value. They only share the _code_.
**useToggle**
The simplest example. It's a true/false switch with a function to flip it.
import { useCallback, useState } from 'react';
export function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((current) => !current), []);
return [on, toggle];
}
'use client';
import { useToggle } from './use-toggle';
export default function Menu() {
const [open, toggleOpen] = useToggle();
return (
<>
<button onClick={toggleOpen}>Menu</button>
{open && <nav>Home | Blog | Contact</nav>}
</>
);
}
**useDebounce**
It waits until the user stops typing before it gives you the new value. It's very useful before sending a search request.
import { useEffect, useState } from 'react';
export function useDebounce(value, delay = 400) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timerId = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timerId); // cancel the old timer on every change
}, [value, delay]);
return debounced;
}
'use client';
import { useState } from 'react';
import { useDebounce } from './use-debounce';
export default function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
// Use debouncedQuery in your fetch or effect. It changes only after
// the user stops typing for half a second.
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<p>Searching for: {debouncedQuery}</p>
</>
);
}
`useDebounce` waits for a fixed amount of time. `useDeferredValue` doesn't use a timer. It lets React decide based on how busy the screen is. Use debounce to slow down network requests, and `useDeferredValue` to keep heavy rendering smooth.
**useFetch**
Here is a fetch Hook with loading and error states, and a cleanup so an old request can't overwrite a newer one.
import { useEffect, useState } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let ignore = false; // becomes true if the url changes or the component leaves
setLoading(true);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error('Request failed');
return res.json();
})
.then((json) => {
if (!ignore) setData(json);
})
.catch((err) => {
if (!ignore) setError(err);
})
.finally(() => {
if (!ignore) setLoading(false);
});
return () => {
ignore = true;
};
}, [url]);
return { data, error, loading };
}
'use client';
import { useFetch } from './use-fetch';
export default function Posts() {
const { data, error, loading } = useFetch(
'https://jsonplaceholder.typicode.com/posts?_limit=5'
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Something went wrong.</p>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
This is great for learning how Hooks fit together. In a real project, fetch in a Server Component, or use a library like TanStack Query or SWR. They handle caching, retries and much more.
The rules for custom Hooks are the same as for all Hooks:
* The name must start with `use`, so React and your linter can recognize it.
* Call Hooks only at the top level, never inside `if`, loops or nested functions.
* Call Hooks only inside components or other custom Hooks.
## **Common Mistakes to Avoid**
**1. Calling a Hook after an early return or inside an`if`**
React remembers your Hooks by the order they are called. If the order changes between renders, everything breaks.
// ❌ Wrong
function Profile({ user }) {
if (!user) return null;
const [name, setName] = useState(user.name); // Hook after an early return
}
// ✅ Right
function Profile({ user }) {
const [name, setName] = useState(user?.name ?? '');
if (!user) return null;
}
The one exception is `use`, which can be called conditionally.
**2. Forgetting`'use client'` in Next.js**
If you use `useState`, `useEffect` or any browser-only Hook in a Server Component, you will see an error saying the Hook only works in a Client Component. Add `'use client'` at the top of that file. Keep those files small, and only mark the interactive parts as client code, so the rest of your page stays on the server.
**3. Using an effect to calculate something**
If you can work it out during rendering, don't use state and an effect for it.
// ❌ Wrong: extra state, extra render
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ✅ Right
const fullName = firstName + ' ' + lastName;
**4. Infinite loops in effects**
// ❌ Wrong: no dependency array, so this runs after every render
// and setCount triggers another render, forever
useEffect(() => {
setCount(count + 1);
});
Another sneaky one: an object or array created during render is a new value every time, so an effect that depends on it re-runs on every render. Move it outside the component, or wrap it in `useMemo`.
**5. Wrapping an input's`setState` in `startTransition`**
Text inputs must update instantly. If you mark that update as low priority, the typing will lag or jump. Keep the input state urgent and use `useDeferredValue` for the slow part.
**6. Calling`useFormStatus` in the same component as the form**
It reads the status of a _parent_ form. Put it in a child component, like the `SubmitButton` from earlier.
**7. Creating a promise while rendering and passing it to`use`**
A new promise is created on every render, so React starts over each time. Create the promise in a Server Component, or use a library that caches it.
**8. Using`useSearchParams` without `Suspense`**
It can cause a build error or turn your whole page into client-rendered content. Wrap the component that uses it in `<Suspense>`.
## **Cheatsheet**
Keep this open while you code. It has every Hook from both parts.
Hook | What it does | Basic syntax
---|---|---
useState | Remembers a value | `const [state, setState] = useState(initial)`
useEffect | Runs side effects | `useEffect(() => {}, [deps])`
useContext | Reads shared data | `const value = useContext(MyContext)`
useRef | Holds a DOM element or a value that doesn't re-render | `const ref = useRef(null)`
useMemo | Remembers a calculated value | `const value = useMemo(() => compute(), [deps])`
useCallback | Remembers a function | `const fn = useCallback(() => {}, [deps])`
useReducer | Handles complex state logic | `const [state, dispatch] = useReducer(reducer, initial)`
useLayoutEffect | Measures the DOM before paint | `useLayoutEffect(() => {}, [deps])`
useImperativeHandle | Exposes methods to a parent | `useImperativeHandle(ref, () => ({ focus() {} }))`
useId | Creates a stable unique ID | `const id = useId()`
useTransition | Marks an update as low priority | `const [isPending, startTransition] = useTransition()`
useDeferredValue | Gives a lagging copy of a value | `const deferred = useDeferredValue(value)`
useSyncExternalStore | Reads data from outside React | `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)`
useInsertionEffect | Injects CSS (for library authors) | `useInsertionEffect(() => {}, [deps])`
useDebugValue | Adds a label in DevTools | `useDebugValue(value)`
useActionState | Tracks the result of a form action | `const [state, formAction, isPending] = useActionState(action, initial)`
useFormStatus | Reads the status of the parent form | `const { pending } = useFormStatus()`
useOptimistic | Shows a temporary value during an Action | `const [optimistic, setOptimistic] = useOptimistic(state, reducer)`
use | Reads a promise or a context | `const data = use(promise)`
useEffectEvent | Reads the latest values inside an effect (19.2+) | `const onEvent = useEffectEvent(() => {})`
useRouter | Navigates from code (Next.js) | `const router = useRouter()`
usePathname | Reads the current path (Next.js) | `const pathname = usePathname()`
useSearchParams | Reads the query string (Next.js) | `const searchParams = useSearchParams()`
useParams | Reads dynamic route params (Next.js) | `const { slug } = useParams()`
useLinkStatus | Knows if a Link is loading (Next.js) | `const { pending } = useLinkStatus()`
useServerInsertedHTML | Inserts HTML on the server (Next.js) | `useServerInsertedHTML(() => html)`
**The rules in one place**
* Call Hooks at the top level of a component or a custom Hook. Never inside `if`, loops or nested functions.
* Custom Hook names start with `use`.
* Hooks with state or effects need `'use client'` in Next.js.
* If you can calculate it during render, skip the effect.
* Always clean up timers and listeners.
* Measure before you reach for `useMemo` and `useCallback`.
## **Conclusion**
That's the full tour of React Hooks. Here is the short version of what to remember:
* **useState, useEffect and useContext** cover most of what you will write every day.
* **useTransition and useDeferredValue** keep your app smooth when updates get heavy.
* **useActionState, useFormStatus and useOptimistic** make forms and instant feedback much simpler in React 19.
* **use and useEffectEvent** remove a lot of old workarounds.
* **Next.js Hooks** handle routing, and they only work in Client Components.
* **Custom Hooks** let you write logic once and use it everywhere.
You don't need to memorize all of them. Learn the core ones well, and come back to the rest when you need them. That's what the cheatsheet is for.
If this guide helped you, leave a ❤️ or a 🦄, and share it with someone who is learning React. Questions, or a Hook I should explain better? Drop a comment below. Thanks for reading! 🙌