Why My Context Provider Was Re-rendering Everything

We added a flame-graph and saw every consumer of `<UserContext>` re-rendering on every keystroke in a sibling. The fix took two days to isolate and three lines to ship.

JavaScript
Frontend
3 snippets
react
hooks
memoization
performance-optimization
amiraprice

By @amiraprice

May 1, 2026

·

Updated May 18, 2026

538 views

5

4.4 (12)

// Reproducing the bug. Every render of the parent allocates a new `value`
// object. Context propagation uses Object.is on that value, so every consumer
// is told to re-render even when nothing it cares about changed.

// Tiny, deterministic React-like harness so we can count consumer renders.
function makeCtx(defaultValue) {
    const ctx = { _value: defaultValue, _consumers: new Set() };
    ctx.Provider = function Provider(value) {
        const lastValue = ctx._value;
        ctx._value = value;
        if (!Object.is(lastValue, value)) ctx._consumers.forEach((fn) => fn(value));
    };
    ctx.subscribe = function (fn) { ctx._consumers.add(fn); fn(ctx._value); };
    return ctx;
}

const UserContext = makeCtx({ user: null, mutate: () => {} });
let consumerRenders = 0;
UserContext.subscribe(() => { consumerRenders++; });

// Parent re-renders three times because of an unrelated state change (typing
// in a search box). Each render allocates a new value object.
function parentRender(unrelatedSearchTerm, user) {
    const mutate = () => {};
    UserContext.Provider({ user, mutate }); // <-- new object every call
}

// Same user reference passed three times. The bug is NOT that user changed,
// it is that { user, mutate } is a fresh literal on every render.
const stableUser = { id: 1 };
parentRender('a', stableUser);
parentRender('ab', stableUser);
parentRender('abc', stableUser);
console.log('consumer renders (buggy):', consumerRenders, '(1 initial subscribe + 3 fan-outs from the value-object reallocation)');
console.log('user identity unchanged across renders, yet consumers re-ran.');

The trap is that JSX hides the allocation. Writing <UserContext.Provider value={{ user, mutate }}> looks declarative, but { user, mutate } is a fresh object literal on every render and React's context machinery uses Object.is to decide whether to fan out an update. The harness above subscribes one consumer and counts how many times it re-runs; we get three updates from three unrelated parent renders even though user was identical. In a real app this manifests as a sluggish form because the search input drags every consumer along for the ride.