When `memo` Actually Stops a Re-render (and When It Does Not)

I once added React.memo everywhere and renders barely changed. Memo only works under specific conditions, and outside those it is dead weight. Three accordions on the trap and the fix.

JavaScript
Frontend
3 snippets
react
hooks
memoization
performance-optimization
ananyaadeyemi

By @ananyaadeyemi

January 8, 2026

·

Updated August 10, 2026

980 views

7

4.2 (12)

// The case memo handles well: a child that takes primitive props and gets
// rendered repeatedly by a parent whose unrelated state is changing. Object.is
// on each prop confirms nothing changed, and the child render is skipped.

// Tiny memo stand-in: shallow-compare props, return cached result on a hit.
function memo(component) {
    let lastProps = null;
    let lastResult = null;
    return function MemoWrapper(props) {
        if (lastProps && shallowEqual(lastProps, props)) return lastResult;
        lastProps = props;
        lastResult = component(props);
        return lastResult;
    };
}

function shallowEqual(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return false;
    const ak = Object.keys(a);
    if (ak.length !== Object.keys(b).length) return false;
    for (const k of ak) if (!Object.is(a[k], b[k])) return false;
    return true;
}

let renderCount = 0;
function Greeting(props) {
    renderCount++;
    return 'Hello, ' + props.name + ' (renders: ' + renderCount + ')';
}
const MemoGreeting = memo(Greeting);

// Parent's unrelated state ticks. Child's only prop is a primitive that does not change.
let parentState = 0;
function parentRender() {
    parentState++;
    return MemoGreeting({ name: 'Ada' });
}

console.log(parentRender());
console.log(parentRender());
console.log(parentRender());
console.log('parent rendered 3 times, child renders:', renderCount);
console.log('memo skipped 2 renders because { name: "Ada" } is shallow-equal each time.');

// When the prop genuinely changes, memo correctly lets the render through.
function parentRenderWithName(name) {
    return MemoGreeting({ name });
}
console.log(parentRenderWithName('Ada'));   // hit, no new render
console.log(parentRenderWithName('Linus')); // miss, child re-renders
console.log(parentRenderWithName('Linus')); // hit again
console.log('child total renders:', renderCount);

This is the case the React docs imply when they introduce memo: the parent re-renders, the child receives the same primitive props, the shallow comparison hits, the child is skipped. The render counter at the bottom is the load-bearing thing to look at. Three parent renders produced one child render, two parent renders with 'Ada' then one with 'Linus' produced one more. If your memo'd components are this shape, memo is doing exactly what you expect. The trick is that almost no real component is this shape, which is what the next accordion is about.