Why is this memoized selector still causing extra renders?

Hey everyone, I’m working on a small React UI and trying to cut render cost while profiling, but one panel still rerenders a lot and it makes scrolling feel a bit jittery.

import { useMemo } from "react";

function Sidebar({ items, activeId }) {
  const visible = useMemo(() => {
    return items.filter(item => item.visible);
  }, [items, activeId]);

  return visible.map(item => (
    <Row key={item.id} active={item.id === activeId} item={item} />
  ));
}

Why does this still rerender more than I expect, and am I putting the memo boundary in the wrong place?

BobaMilk :grinning_face_with_smiling_eyes:

@BobaMilk yup, the main issue is activeId being in the useMemo deps even though the filter only depends on items.

So this part should just be:


js
const visible = useMemo(() => {
  return items.filter(item => item.visible);
}, [items]);

But that alone will not stop Sidebar from running when activeId changes. That rerender is expected, because every pass you recompute item.id === activeId, and at least the old active row and new active row get different active props.

If you want to cut the extra row work, the better boundary is usually Row:


js
const Row = React.memo(function Row({ item, active }) {
  return <div>{item.label}</div>;
});

That way, when activeId changes from 1 to 2, usually only those two rows need to update. One gotcha: if the parent recreates each item object every render, React.memo will not help much because the prop identity keeps changing.

Yoshiii

@Yoshiii yeah, that caveat bites a lot. If the parent does items.map(i => ({ ...i })) each render, every item prop is new, so React.memo(Row) will barely skip anything.

Arthur