React memo, useMemo and useCallback (with Refs, Portals and useId)

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.

One line each
APICaches
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.

Decision chart: memo, useMemo, useCallback or something else

Which tool for which problem
SymptomReach forWhy
A child re-renders when its parent does, though its props did not changememo(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 timeuseCallbackKeeps the same function between renders
A memo child still re-renders because you pass a new object or arrayuseMemoKeeps the same object until its inputs change
A slow calculation runs on every keystrokeuseMemoCaches the result until its inputs change
Thousands of rows make scrolling or typing slowA virtual list (react-window)Only rows on screen are in the DOM
Typing lags because a big list updates with ituseDeferredValue or startTransitionLets the input update first and the list catch up
Nothing is visibly slowNothingMemoising has a cost too; measure first

Render counts, before and after

memo: skip renders when props are equal

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} />
    </>
  );
}

useCallback: keep a function the same

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} />)}
    </>
  );
}

useMemo: cache a slow calculation

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],
);

forwardRef, ref as a prop, portals and useId

forwardRef (React 18 and earlier)

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>
    </>
  );
}

ref as a prop (React 19)

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" />

createPortal for modals and tooltips

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 for label and input pairs

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>
  );
}

Long lists

react-window: a list of 10,000 rows

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 }}
    />
  );
}

How to measure

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.

Common mistakes

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.

How it works

  1. Find the slow interaction with the React DevTools Profiler.
  2. Check the decision chart for the symptom you see.
  3. Apply one change, such as memo on the child that re-renders.
  4. Stabilise the props it receives with useCallback or useMemo.
  5. Profile again and keep the change only if the numbers improved.

Tips

Frequently asked questions

What is the difference between useMemo and useCallback?

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).

Should I wrap every component in memo?

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.

Is forwardRef deprecated?

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.

Why use a portal for a modal?

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.

Can I use useId for list keys?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap