4

I m having one child component which is inside a loop of parent component. when one of the child components is updating the state of parent component, it is re-rendering the all children since it is loop. How can i avoid the re-render for each iteration.


function Parent() {
  const [selectedChild, setSelectedChild] = useState([]);

  const onChangeHandle = (event, id) => {
    const checked = event.target.checked;
      let updatedArray = [...selectedChild];
      if(checked){
         if(!selectedChild.includes(id)){
            updatedArray.push(id); 
         }
      }
      else{
         var index = updatedArray.indexOf(id)
         if (index !== -1) {
            updatedArray.splice(index, 1);
         }
      }
      setSelectedChild(updatedArray);
  }
  const dummy = (id) => {
    return selectedChild.includes(id);
  }
  return (
    <div>
    <table>
    <tbody>
      {[1,2,3].map((value, index) => {
      return (
        <Child 
        key={index} 
        index={index} 
        value={value} 
        handle={onChangeHandle}
        isSelected={dummy}
        />
      )
      })}
    </tbody>
    </table>
    <div>
      {selectedChild}
    </div>
  </div>)
}

function Child({index, value, handle, isSelected }) {
  console.log('rendering')

 return (
 <tr>
    <td>
      <input 
      type="checkbox" 
      checked={isSelected(index)}
      onChange={(event) => handle(event, index)}/>
    </td>
    <td>hello {index} {value}</td>
 </tr>
 )
}

export default function App() {
  return (
    <div className="App">
      <Parent />
    </div>
  );
}

Current behaviour: In above code, When i m clicking on the checkbox in one of the children component, it is updating the parent component state(selectedChild). So the loop is executing and all children(all table rows) are re rendering.

Expected behaviour: Only that particular row have to go for re-render

Demo: https://codesandbox.io/s/newpro-0pezc

SuganthiRaj
  • 63
  • 1
  • 6
  • Well for one thing, try to use unique key property values [rather than array indicies](https://reactjs.org/docs/lists-and-keys.html#keys). That can definitely avoid some re-renders even if it isn't your specific problem here. – Jared Smith Jan 08 '21 at 14:05
  • 1
    I'm not going to vote to close this as a duplicate, but does [this answer your question](https://stackoverflow.com/questions/54551949/react-hooks-how-do-i-implement-shouldcomponentupdate)? – Jared Smith Jan 08 '21 at 14:07

3 Answers3

4

for that you can use React.memo that will memoize your component if props remains the same. But given your code you need to make some extra changes:

  • you have to apply useCallback to memoize onChangeHandle function;

  • to memoize properly onChangeHandle you need to refactor it. you can't pass selectedChild directly, otherwise it memoizes its value. use setSelectedChild passing as argument a function that takes selectedChild instead.

  • your Child should receive isSelected as boolean value instead of function. otherwise props will remain the same and Child never updates;

    import React, { useState, memo, useCallback } from "react";
    
    function Parent() {
      const [selectedChild, setSelectedChild] = useState([]);
    
      const onChangeHandle = useCallback((event, id) => {
        setSelectedChild(selectedChild => {
          const checked = event.target.checked;
          let updatedArray = [...selectedChild];
          if (checked) {
            if (!selectedChild.includes(id)) {
              updatedArray.push(id);
            }
          } else {
            var index = updatedArray.indexOf(id);
            if (index !== -1) {
              updatedArray.splice(index, 1);
            }
          }
          return updatedArray;
        });
      }, []);
    
      const dummy = id => {
        return selectedChild.includes(id);
      };
    
      const renderChildren = () =>
        [1, 2, 3].map((value, index) => {
          return (
            <Child
              key={index}
              index={index}
              value={value}
              handle={onChangeHandle}
              isSelected={dummy(index)}
            />
          );
        });
    
      return (
        <div>
          <table>
            <tbody>{renderChildren()}</tbody>
          </table>
          <div>{selectedChild}</div>
        </div>
      );
    }
    
    const Child = memo(({ index, value, handle, isSelected }) => {
      console.log("rendering");
    
      return (
        <tr>
          <td>
            <input
              type="checkbox"
              checked={isSelected}
              onChange={event => handle(event, index)}
            />
          </td>
          <td>
            hello {index} {value}
          </td>
        </tr>
      );
    });
    
    export default function App() {
      return (
        <div className="App">
          <Parent />
        </div>
      );
    }
    

https://stackblitz.com/edit/so-memo-children?file=src/App.js

buzatto
  • 9,704
  • 5
  • 24
  • 33
  • interesting use of a setter callback. +1 – marzelin Jan 08 '21 at 15:21
  • 1
    btw: a function has parameters, but you pass arguments to a function, not parameters. Passed arguments are assigned to parameters when a function is called. – marzelin Jan 08 '21 at 15:27
  • @buzatto - your answer helped me. But I have little change in my usecase. can you see this https://stackoverflow.com/questions/66046375/how-to-render-only-subchild-without-rendering-all-high-level-component – SuganthiRaj Feb 04 '21 at 13:17
1

The basic answer is use React.memo on Child.

const Child = memo(function Child(...) {...})

But to make memo work, the component needs to receive the same props if it shouldn't get rerendered. That means using useCallback on onChangeHandle:

 const onChangeHandle = useCallback((event, id) => {...}, [])

But since onChangeHandle uses selectedChild that always changes on checkbox change, you'll also need to ref it using useRef:

const selectedChildRef = useRef();
selectedChildRef.current = selectedChild;

and use reffed version inside of onChangeHandle.

The last thing that needs to be done is to change isSelected prop from function to just a flag since it needs to be run on each checkbox change:

isSelected={selectedChild.includes(index)}

https://codesandbox.io/s/newpro-forked-wxvqs

marzelin
  • 10,790
  • 2
  • 30
  • 49
  • at first glance I didn't think about useRef, that's a solid approach. – buzatto Jan 08 '21 at 15:03
  • @buzatto: I have observed using useRef() actually doesn't throw dependency error in the React Usecallback, can you please guide me on why it happens that way? The reason I'm asking is, in my implementation I don't need to rely on refs, just to solve that dependency error I'm using it. here is my POC Link: [link](https://codesandbox.io/s/performance-optim-list-rendering-psqms?file=/src/List.js) – Vanga Kiran Kumar Reddy Jul 15 '21 at 04:04
  • similarly to setters, `refs` are *stable* (the return value of `useRef` is always the same object) so you don't have to put them in the deps and `ref.current` will always store the latest value. – marzelin Jul 15 '21 at 11:20
  • you can use setState updater function instead of relying on `ref` – marzelin Jul 15 '21 at 11:33
0

You could implement shouldComponentUpdate (doc: https://reactjs.org/docs/react-component.html#shouldcomponentupdate) inside the definition of Child to have more control over when it rerenders. But that's only meant for cases where you have performance issues- generally you don't have to worry about it, and letting them all rerender is standard.

lt1
  • 761
  • 5
  • 11