Wrap a component in memo to skip renders when its props are unchanged. Use useCallback to keep a function prop the same between renders, and useMemo to cache an object or a slow calculation. They matter only together with memo or a real cost. Measure first with the React Profiler; the live counters below show the difference.
| API | Caches |
|---|---|
| memo(Component) | A component's render output |
| useMemo(fn, deps) | A value |
| useCallback(fn, deps) | A function |
| createPortal(node, el) | Nothing; renders elsewhere in the DOM |
| useId() | Nothing; makes a stable unique id |
Updated
The demos count real renders in your browser. Type in each box with the switch off and on, and watch the counters. The second half covers the reference APIs that come up next to performance work: refs, portals and ids.
Building forms? useId pairs well with React Hook Form.
| Symptom | Reach for | Why |
|---|---|---|
| A child re-renders when its parent does, though its props did not change | memo(Child) | Skips the render when every prop is equal (Object.is) |
| A memo child still re-renders because you pass it a new function each time | useCallback | Keeps the same function between renders |
| A memo child still re-renders because you pass a new object or array | useMemo | Keeps the same object until its inputs change |
| A slow calculation runs on every keystroke | useMemo | Caches the result until its inputs change |
| Thousands of rows make scrolling or typing slow | A virtual list (react-window) | Only rows on screen are in the DOM |
| Typing lags because a big list updates with it | useDeferredValue or startTransition | Lets the input update first and the list catch up |
| Nothing is visibly slow | Nothing | Memoising has a cost too; measure first |
Type in the box. Without memo, the list re-renders on every key even though its props did not change. With memo it renders once.
import { memo, useState } from "react";
const List = memo(function List({ items }) {
return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>;
});
const ITEMS = ["Menu", "Hours", "Contact"]; // stable: defined once, outside the component
export function Page() {
const [q, setQ] = useState("");
return (
<>
<input value={q} onChange={(e) => setQ(e.target.value)} />
<List items={ITEMS} />
</>
);
}Passing an inline arrow breaks memo, because a new function is created each render. useCallback returns the same one until its dependencies change.
const Row = memo(function Row({ label, onPick }) {
return <button onClick={() => onPick(label)}>{label}</button>;
});
export function Picker() {
const [text, setText] = useState("");
const [picked, setPicked] = useState(null);
// Without useCallback: onPick={(l) => setPicked(l)} re-renders every Row on each key.
const onPick = useCallback((label) => setPicked(label), []);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
{["Small", "Medium", "Large"].map((l) => <Row key={l} label={l} onPick={onPick} />)}
</>
);
}Filtering 20,000 products on every render is wasted work when only an unrelated value changed. useMemo reruns the filter only when products or query change.
const visible = useMemo(
() => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
[products, query],
);A parent cannot pass ref to a function component unless the component forwards it. forwardRef hands the ref through to a real element.
import { forwardRef, useRef } from "react";
const TextInput = forwardRef(function TextInput(props, ref) {
return <input ref={ref} {...props} />;
});
export function Search() {
const input = useRef(null);
return (
<>
<TextInput ref={input} placeholder="Search" />
<button onClick={() => input.current.focus()}>Focus search</button>
</>
);
}From React 19, function components receive ref like any other prop, so forwardRef is no longer needed in new code.
// React 19
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
<TextInput ref={input} placeholder="Search" />A portal renders children into another DOM node, usually document.body, so a modal escapes parents with overflow hidden or a low z-index. React events still bubble through the React tree.
import { createPortal } from "react-dom";
function Modal({ open, onClose, children }) {
if (!open) return null;
return createPortal(
<div className="backdrop" onClick={onClose}>
<div role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.body,
);
}useId gives a unique id that matches between server and browser render, so several copies of the same field component never clash.
import { useId } from "react";
function Field({ label, hint }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} aria-describedby={`${id}-hint`} />
<p id={`${id}-hint`}>{hint}</p>
</div>
);
}List renders only the rows in view plus a few extra. Each row gets index and style; style positions the row and must be applied. The demo beside it uses the same idea, written by hand.
import { List } from "react-window";
function Row({ index, style, items }) {
return <div style={style}>{items[index].name}</div>;
}
export function Customers({ items }) {
return (
<List
rowComponent={Row}
rowCount={items.length}
rowHeight={36}
rowProps={{ items }}
style={{ height: 320 }}
/>
);
}
Open React DevTools, go to the Profiler tab, record while you do the slow thing, and stop. The flame chart shows which components rendered and how long each took. Turn on "Highlight updates when components render" to see re-renders as you click.
Test with a production build. Development mode adds checks that make everything slower, and StrictMode renders components twice on purpose.
Listing an object that is recreated each render as a dependency, so useMemo recalculates every time anyway.
Leaving out a dependency to stop a recalculation, which shows stale data. The lint rule react-hooks/exhaustive-deps catches this.
Putting a portal's target node in state that is not ready on the first render, so the modal flashes or throws on the server.
useMemo calls your function and caches the value it returns. useCallback caches the function itself. useCallback(fn, deps) is the same as useMemo(() => fn, deps).
No. Comparing props costs time too, and most renders are cheap. Use memo where the Profiler shows a component rendering often with the same props and taking noticeable time.
In React 19 function components accept ref as a normal prop, so new code does not need forwardRef. It still works, and the React team has said it will be deprecated in a future release.
A modal inside a card with overflow hidden or a transform can be clipped or stacked under other content. Rendering it into document.body with createPortal avoids both while keeping it in your React tree.
No. useId is for accessibility attributes such as htmlFor and aria-describedby. Keys should come from your data, such as a database id.
Related: React Router examples, Framer Motion examples, React Hook Form examples, React icons, All free tools.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.