React best practice for changing state of parent component from child without rerendering all children?

Viewed 508

I have a project where I'm displaying cards that contain attributes of a person in a textfield, and the user can edit the textfield to directly change that person's attribute values. However every time they're editing the textfield it causes a rerender of all cards which slows down the app. Here is an example:

export default Parent() {  
    const [personList, setPersonList] = useState(/* list of person objects*/);
    const modifyPerson(index, property, value) {
          const newPersonList = _.cloneDeep(personList);
          newPersonList[index][property] = value;
          setPersonList(newPersonList);
    }

    const children = personList.map((person, index) => {
           <Person
               modifyPerson={modifyPerson}
               index=index
               /*properties on the person */ 
           />
    });

    return <div> {children} </div>
}

export default Person(props) {
    
    const fields = /* assume a list of these textfields for each property */
           <TextField 
            value={props.name}
            onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
            value={props.name} >
    return {fields};
}

So essentially when the child's text field is updated, it triggers a state change in the parent that stores the new value, then refreshes what the Child looks like. There's no button that the user clicks to "save" the values-- as soon as they edit the textfield it's a permanent change. And also the parent needs to know the new values of every Person because there's some functions that require knowledge of the current state of the person list. The Person component contains images that slow down the rendering if done inefficiently.

Is there a better way to make this design more performant and reduce rerenders? I attempted to use useCallback to preserve the functions but I don't it works in this specific design because the property and values are different-- do I have to create a new "modifyPerson" for each exact attribute?

2 Answers

Use React.memo()

React.Memo will check the props passed to the component and only if the props changes , it will re-render that particular component.

So, if you have multiple Person component which are getting props which are explicit to those Person, and when you change a particular Person component which leads to Parent state getting updated, only that Person component which you had modified will re-render because its props will change(assuming that you pass the changed value to it).

The other components will have the same props and wont change.

export default React.memo(Person(props) {
    
    const fields = /* assume a list of these textfields for each property */
           <TextField 
            value={props.name}
            onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
            value={props.name} >
    return {fields};
})

As others have already said React.memo() is the way to go here, but this will still re-render because you recreate modifyPerson every time. You can use useCallback to always get the same identity of the function, but with your current implementation you would have to add personList as dependency so that doesn't work either.

There is a trick with setPersonList that it also accepts a function that takes the current state and returns the next state. That way modifyPerson doesn't depend on any outer scope (expect for setPersonList which is guaranteed to always have the same identity by react) and needs to be created only once.

const modifyPerson = useCallback((index, property, value) {
    setPersonList(currentPersonList => {
        const newPersonList = _.cloneDeep(currentPersonList);
        newPersonList[index][property] = value;
        return newPersonList;
    })
}, []);
Related