Search code examples
reactjsreact-hooksmemoization

ReactJS how to memoize within a loop to render the same component


I have a component that creates several components using a loop, but I need to rerender only the instance being modified, not the rest. This is my approach:

    function renderName(item) {
       return (
          <TextField value={item.value || ''} onChange={edit(item.id)} />
       );
    }
    
    function renderAllNames(items) {
       const renderedItems = [];
       items.forEach(x => {
          const item = React.useMemo(() => renderName(x), [x]);
          renderedItems.push(item);
       });
       return renderedItems;
    };
    
    return (
      <>
         {'Items'}
         {renderAllNames(names)};
      </>
    );

This yells me that there are more hooks calls than in the previous render. Tried this instead:

function renderAllNames(items) {
           const renderedItems = [];
           items.forEach(x => {
              const item = React.memo(renderName(x), (prev, next) => (prev.x === next.x));
              renderedItems.push(item);
           });
           return renderedItems;
        };

Didn't work either... the basic approach works fine

function renderAllNames(items) {
           const renderedItems = [];
           items.forEach(x => {
              renderedItems.push(renderName(x));
           });
           return renderedItems;
        };

But it renders all the dynamic component everytime I edit any of the fields, so how can I get this memoized in order to rerender only the item being edited?


Solution

  • You're breaking the rules of hooks. Hooks should only be used in the top level of a component so that React can guarantee call order. Component memoisation should also really only be done using React.memo, and components should only be declared in the global scope, not inside other components.

    We could turn renderName into its own component, RenderName:

    function RenderName({item, edit}) {
        return (
          <TextField value={item.value || ''} onChange={() => edit(item.id)} />
       );
    }
    

    And memoise it like this:

    const MemoRenderName = React.memo(RenderName, (prev, next) => {
      const idEqual = prev.item.id === next.item.id;
      const valEqual = prev.item.value === next.item.value;
      const editEqual = prev.edit === next.edit;
    
      return idEqual && valEqual && editEqual;
    });
    

    React.memo performs strict comparison on all the props by default. Since item is an object and no two objects are strictly equal, the properties must be deeply compared. A side note: this is only going to work if edit is a referentially stable function. You haven't shown it but it would have to be wrapped in a memoisation hook of its own such as useCallback or lifted out of the render cycle entirely.

    Now back in the parent component you can map names directly:

    return (
      <>
         {'Items'}
         {names.map(name => <MemoRenderName item={name} edit={edit}/>)}
      </>
    );