# New react callbacks memoization pattern

# How to stabilise a memo component when everything else fails

`memo` looks simple until you wrap a component in it and it re-renders anyway.

So here is the question underneath this whole pattern:

```plaintext
How do you give a memoized child function props that never change identity,
when the parent rebuilds those functions on every render
and you can't refactor the parent to stop?
```

Let me trace it with one example, all the way through.

## The running example

A `<DataGrid>` that renders thousands of cells. Expensive. A perfect `memo` candidate.

Its parent owns the state and passes handlers down:

```tsx
const Page = () => {
  const [query, setQuery] = useState('')
  const [selected, setSelected] = useState<Row[]>([])

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />

      <DataGrid
        rows={filterRows(query)}
        selected={selected}
        onSelect={(row) => setSelected([...selected, row])}
        onClearSelection={() => setSelected([])}
        onDeleteSelected={() => deleteRows(selected)}
        onSortColumn={(col) => sortBy(col)}
      />
    </>
  )
}

const DataGrid = memo((props: Props) => {
  // thousands of cells
})
```

The handlers are not independent.

`onSelect` reads and replaces `selected`.

`onClearSelection` replaces `selected`.

`onDeleteSelected` reads `selected`.

They are interconnected. They all close over the same state. Hold onto that — it is what breaks the easy fix.

## Watch memo fail

`memo` compares the new props to the old props before re-rendering.

Shallowly. Key by key. With `Object.is`.

Same reference for every prop → it skips the render.

One new reference → it renders.

Type a single character in the search box:

```plaintext
keystroke
↓
setQuery
↓
Page re-renders
↓
every inline arrow is created fresh: onSelect, onClearSelection, onDeleteSelected, onSortColumn
↓
memo compares: onSelect !== last onSelect
↓
DataGrid re-renders all of its cells
```

A function created inline is a new object every render.

`(row) => ...` today is not `Object.is`\-equal to `(row) => ...` yesterday.

The shallow compare fails on the first callback it checks.

The expensive render runs anyway.

## Why useCallback can't save this one

The textbook fix is `useCallback`: it hands back the same function object between renders, so the comparison passes.

Wrap `onSelect`:

```tsx
const onSelect = useCallback(
  (row) => setSelected([...selected, row]),
  [selected]
)
```

The callback closes over `selected`, so `selected` goes in the dependency array.

Which means `onSelect` becomes a new object every time `selected` changes.

Which is every time you select a row.

`memo` breaks again, for the exact reason it was breaking before.

Drop the dependency instead:

```tsx
const onSelect = useCallback(
  (row) => setSelected([...selected, row]),
  [] // stable, but now wrong
)
```

The reference is stable now. But the closure is frozen at the first render.

`selected` is always the empty array it was on mount.

Selecting a second row throws away the first.

That is a bug.

This is the trap:

```plaintext
Put the deps in   → the reference changes → memo breaks.
Leave the deps out → the closure goes stale → bug.
```

`useCallback` alone cannot give you a stable reference *and* fresh values.

These are just some shallow examples to make a point, but imagine that you have some thousands of lines of code and over thirty interconnected handlers with lots of props themselves. To fix the grid you'd have to get the dependency arrays right across all of them, keep them in sync as the code changes, and probably lift state or thread it through reducers.

That is a real refactor of the parent which I'm not against, but sometime in the real world, you don't have the time to do this.

## The escape is a ref

You need one thing the trap denies you: a value that is **stable in identity** but **reads fresh logic**.

A function whose reference never changes.

That, when called, runs the newest version of the code.

A plain variable can't do that. `useCallback` can't do that.

A ref can.

A ref is a box with a stable identity whose contents you can swap.

So:

```plaintext
Hand the child a function that never changes.
Have that function read the real callback out of a ref at call time.
Update the ref to the latest props on every render.
```

The child sees a frozen reference.

The ref always holds the latest logic.

The two are decoupled.

## The hook

`useStablePropsCallbacks` does this for every function prop at once:

```tsx
import { useRef } from 'react'

type AnyCallback = (...args: unknown[]) => unknown

// A function of any shape — only used to detect which props are callbacks.
type AnyFunction = (...args: any[]) => any

// The prop names in P whose value is a function.
// Map each key to itself-or-never, then `[keyof P]` collapses it to the union of survivors.
type CallbackKeys<P> = {
  [K in keyof P]: P[K] extends AnyFunction ? K : never
}[keyof P]

// P with only those props kept.
type CallbacksOf<P> = Pick<P, CallbackKeys<P>>

const isCallbackProp = <P extends object>(
  propKey: string,
  props: P
): propKey is string & keyof CallbacksOf<P> =>
  typeof (props as Record<string, unknown>)[propKey] === 'function'

export const useStablePropsCallbacks = <P extends object>(
  props: P
): CallbacksOf<P> => {
  // Refreshed every render so the proxies reach the newest callbacks.
  const latestPropsRef = useRef(props)
  latestPropsRef.current = props

  // The proxy object. Built once, never rebuilt.
  const stableCallbacksRef = useRef<CallbacksOf<P> | null>(null)

  if (!stableCallbacksRef.current) {
    const proxies = {} as Record<string, AnyCallback>

    for (const propKey of Object.keys(props)) {
      if (!isCallbackProp(propKey, props)) continue

      proxies[propKey] = (...args: unknown[]) => {
        const latestProps = latestPropsRef.current as Record<string, unknown>
        const latestCallback = latestProps[propKey] as AnyCallback
        return latestCallback(...args)
      }
    }

    stableCallbacksRef.current = proxies as CallbacksOf<P>
  }

  return stableCallbacksRef.current
}
```

`CallbacksOf<P>` is just "`P` with the non-function props removed" — it types the return value as only the callbacks. The runtime hook is three moving parts.

`latestPropsRef` holds the latest props. `latestPropsRef.current = props` runs on every render, so the box always contains the newest callbacks. (Writing a ref during render is normally a yellow flag; it is safe here because the proxies only ever read "whatever is latest" — they never snapshot a value at a particular render.)

`stableCallbacksRef` holds the proxy object. The `if (!stableCallbacksRef.current)` block runs only on the first render. After that the same object comes back forever.

**Each proxy** is, for one function prop, a wrapper that resolves the real callback at call time: read the latest props, pick the callback for this key, forward the args. Its identity is fixed for the life of the component, but the function it forwards to is always the current one.

## Wiring it in

The hook returns stable versions of the function props. Spread them over the real props so they win:

```tsx
const StableDataGrid = (props: Props) => {
  const stableCallbacks = useStablePropsCallbacks(props)
  return <DataGrid {...props} {...stableCallbacks} />
}
```

Order matters. `{...props}` puts the raw, unstable callbacks in first. `{...stableCallbacks}` then overrides each function prop with its stable proxy.

A thin outer wrapper stabilizes the references. The memoized inner component receives the stable ones.

## The flow, end to end

Type a character in the search box again. This time:

```plaintext
keystroke
↓
setQuery → Page re-renders → new inline callbacks (as before)
↓
StableDataGrid runs
↓
latestPropsRef.current = props          (every render — ref now holds the new callbacks)
↓
proxy object returned                   (same object as last render — built once at mount)
↓
DataGrid receives the same proxy references it saw last time
↓
memo shallow compare passes → DataGrid skips its render
↓
later, user clicks a row → proxy onSelect fires
↓
proxy reads latestPropsRef.current.onSelect → runs the newest closure → fresh `selected`
```

The expensive grid stayed put through every keystroke.

When a real interaction happened, the call hit the current logic, with the current state.

Stable identity for `memo`. Fresh closure for correctness. No `useCallback`, no dependency arrays, no parent refactor.

## What this gives up

**The callback set is frozen at mount.** `Object.keys(props)` runs once, inside the `if` block. A handler that is `undefined` on the first render and supplied later never gets a proxy. If your function props are all present from the start, you are fine. If they appear conditionally, they slip through.

**Only function props are stabilized.** Object and array props still get new references when the parent recreates them, and will still break `memo`. Those need their own handling.

**The indirection costs you some tooling.** Stack traces pass through the proxy. And because the wiring is dynamic, the `react-hooks/exhaustive-deps` lint can't see it — you trade a class of dependency-array bugs for a class of "invisible to the linter" ones.

## When not to reach for it

One or two callbacks? Just use `useCallback`. It is clearer, and the linter helps you.

This pattern earns its keep in one narrow situation: many interconnected, stateful callbacks, on a genuinely expensive child, where rewriting the parent to hand-stabilize each one isn't worth it.

Inside that window, it is the difference between `memo` working and `memo` being decoration. Outside it, it is indirection you don't need.

The whole thing in three beats:

```plaintext
The proxy holds a stable identity.
The ref holds the latest logic.
The memo holds its ground.
```

* * *

# Appendix: the types, traced one step at a time

Three lines of types do the heavy lifting in that hook, and they are where most eyes glaze over. Mine did.

```ts
type CallbackKeys<P> = {
  [K in keyof P]: P[K] extends AnyFunction ? K : never
}[keyof P]

type CallbacksOf<P> = Pick<P, CallbackKeys<P>>

const isCallbackProp = <P extends object>(
  propKey: string,
  props: P
): propKey is string & keyof CallbacksOf<P> =>
  typeof (props as Record<string, unknown>)[propKey] === 'function'
```

The `P` is the part that makes it abstract. But `P` is not a real type — it is a placeholder. A fill-in-the-blank.

It works exactly like a function parameter, one level up. A function parameter stands for whatever *value* you pass in. `P` is a *type* parameter: it stands for whatever *type* you pass in.

And you pass a type in by writing it in the angle brackets. `CallbacksOf<Props>` hands `Props` to the blank called `P`. From that point on, TypeScript substitutes `Props` for `P` everywhere in the definition — `keyof P` becomes `keyof Props`, `P[K]` becomes `Props[K]`, `Pick<P, …>` becomes `Pick<Props, …>`.

Here is the type we fill it with:

```ts
type Props = {
  selected: Row[]
  onSelect: (row: Row) => void
  onClearSelection: () => void
  count: number
}
```

Two functions. Two non-functions. We want a type that keeps only the two functions.

For every example below, the rule is simple: wherever a definition says `P`, read `Props`.

One thing to hold in your head before we start: none of this runs.

These types are erased before your code executes. There is no `CallbackKeys` object at runtime, no loop, no work. This is the compiler reasoning about shapes — all of it happens in your editor and at build time, then vanishes. So when I say "the value becomes a name," I mean in the type, on the compiler's scratch pad. Nothing is computed when the app runs.

## CallbackKeys: filtering keys when the language has no filter

You'd think you could just write `keyof Props`.

`keyof Props` gives you the names of every prop, as a union:

```plaintext
"selected" | "onSelect" | "onClearSelection" | "count"
```

But that is all four. We only want the two that are functions.

And here is the problem: TypeScript has no `.filter()` for keys. You cannot say "keep the keys where the value is a function." There is no such operator.

So you do it sideways. In three moves.

**Move 1 — walk every key and replace its value.**

```ts
{
  [K in keyof P]: P[K] extends AnyFunction ? K : never
}
```

`[K in keyof P]` means "for each key `K` in `Props`, make an entry."

The value of each entry is not the original type. It is `P[K] extends AnyFunction ? K : never` — a question asked per key:

```plaintext
Is this prop's value a function?
Yes → put the key's own name as the value.
No  → put `never` as the value.
```

So the object type you get is:

```ts
{
  selected: never                          // Row[] is not a function
  onSelect: "onSelect"                      // a function → its own name
  onClearSelection: "onClearSelection"      // a function → its own name
  count: never                              // number is not a function
}
```

Read that again. The function keys now hold their *own name* as a value. The rest hold `never`.

**Move 2 — read all the values at once.**

`[keyof P]` on the end looks like indexing an array. It is indexing a type.

When you index an object type by a single key, you get that key's value. When you index it by a *union* of keys, you get the union of all those values.

`keyof Props` is the union of all four keys, so:

```plaintext
{…}["selected" | "onSelect" | "onClearSelection" | "count"]
```

hands back the union of the four values:

```plaintext
never | "onSelect" | "onClearSelection" | never
```

**Move 3 —** `never` **falls out for free.**

`never` is the empty type. In a union it just disappears — `never | "onSelect"` is `"onSelect"`.

So the whole thing collapses to:

```plaintext
"onSelect" | "onClearSelection"
```

That is `CallbackKeys<Props>`. The names of the function props, and nothing else.

The flow, end to end:

```plaintext
Props
↓ keyof P
"selected" | "onSelect" | "onClearSelection" | "count"
↓ map each key → (its own name if a function, else never)
{ selected: never, onSelect: "onSelect", onClearSelection: "onClearSelection", count: never }
↓ [keyof P] — read every value as one union
never | "onSelect" | "onClearSelection" | never
↓ never drops out of a union
"onSelect" | "onClearSelection"
```

The map-to-name-then-index flow *is* the missing `filter`. That is the only reason it exists.

## CallbacksOf: turning the names back into an object

Now you have the names. You want the object.

```ts
type CallbacksOf<P> = Pick<P, CallbackKeys<P>>
```

`Pick<T, K>` is built into TypeScript. It means exactly what it says: pick keys `K` out of object `T`, keep their types, drop everything else.

So `Pick<Props, "onSelect" | "onClearSelection">` is:

```ts
{
  onSelect: (row: Row) => void
  onClearSelection: () => void
}
```

`Props` with the non-functions gone. The values come through untouched.

That is `CallbacksOf<Props>`. One readable line, because `CallbackKeys` already did the hard part.

## isCallbackProp: a runtime check that teaches the compiler something

This one mixes two worlds, which is what makes it look strange.

```ts
const isCallbackProp = <P extends object>(
  propKey: string,
  props: P
): propKey is string & keyof CallbacksOf<P> =>
  typeof (props as Record<string, unknown>)[propKey] === 'function'
```

The two worlds are both in the signature, and they are easy to mix up because they look alike.

`props` — lowercase — is the real object your code holds at runtime: the actual `{ selected, onSelect, … }`.

`P` — uppercase — is its *type*: `Props`, the same thing we filled in earlier.

`props: P` just says "this value has that shape." Lowercase is the value, uppercase is the shape of the value. So in our example, `props` is an object and `P` is `Props`.

Ignore the return type for a second. The body is trivial:

```plaintext
typeof props[propKey] === 'function'
```

"Is the value at this key a function?" A plain boolean. That part runs at runtime, in the browser, while the hook builds its proxies.

Now the strange part: the return type is not `boolean`. It is `propKey is string & keyof CallbacksOf<P>`.

That is a **type predicate**. It changes what the compiler knows about `propKey` after the call.

Here is why you need it. Inside the hook:

```ts
for (const propKey of Object.keys(props)) {
  if (!isCallbackProp(propKey, props)) continue
  // ...
}
```

`Object.keys` returns `string[]`. So `propKey` starts as a plain `string`. The compiler has no idea *which* key it is — it could be `"count"`, which is not a callback.

A naive version returning `: boolean` would check the value at runtime, but the compiler would still see `propKey` as a plain `string` afterward. It would not let you treat it as a callback key.

The `is` predicate fixes that. It tells the compiler:

```plaintext
If this function returns true, then for the code that follows,
you may treat `propKey` as `string & keyof CallbacksOf<P>`.
```

For our `Props`, `keyof CallbacksOf<Props>` is `"onSelect" | "onClearSelection"`. So after the `if`, the compiler narrows `propKey` from "some string" to "one of the callback names." Now indexing the callbacks object with it is safe.

The `string &` part is a small guard. `keyof` can in general yield `string | number | symbol`, but `Object.keys` only ever gives strings — intersecting with `string` keeps the result a string key. For our `Props` the keys are already strings, so it changes nothing here; it matters only for the fully generic `P`.

Now the honest part, because a predicate has a sharp edge.

The body runs. The predicate does not.

`typeof … === 'function'` is real work the browser does. `propKey is …` is a *promise you make to the compiler*, and the compiler believes it without checking. If you wrote a boolean that did not actually match the promise, TypeScript would trust the lie and narrow anyway.

Here the boolean and the promise agree — a prop whose value is a function genuinely is a callback key — so it is sound. But that is on you to get right, not something the compiler proves.

The three types in three beats:

```plaintext
keyof P lists every prop name.
CallbackKeys filters those names down to the callbacks.
isCallbackProp proves, at runtime, that one loose string is really one of them.
```
