Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to optimize React components with React.memo and useCallback when callbacks are changing state in the parent

I've come accross a performance optimization issue that I feel could be fixed somehow but I'm not sure how.

Suppose I have a collection of objects that I want to be editable. The parent component contains all objects and renders a list with an editor component that shows the value and also allows to modify the objects.

A simplified example would be this :

import React, { useState } from 'react'

const Input = props => {

    const { value, onChange } = props

    handleChange = e => {
        onChange && onChange(e.target.value)
    }

    return (
        <input value={value} onChange={handleChange} />
    )
}

const ObjectEditor = props => {
    const { object, onChange } = props

    return (
        <li>
            <Input value={object.name} onChange={onChange('name')} />
        </li>
    )
}

const Objects = props => {

    const { initialObjects } = props

    const [objects, setObjects] = useState(initialObjects)

    const handleObjectChange = id => key => value => {
        const newObjects = objects.map(obj => {
            if (obj.id === id) {
                return {
                    ...obj,
                    [key]: value
                }
            }
            return obj
        })
        setObjects(newObjects)
    }

    return (
        <ul>
            {
                objects.map(obj => (
                    <ObjectEditor key={obj.id} object={obj} onChange={handleObjectChange(obj.id)} />
                ))
            }
        </ul>
    )
}

export default Objects

So I could use React.memo so that when I edit the name of one object the others don't rerender. However, because of the onChange handler being recreated everytime in the parent component of ObjectEditor, all objects always render anyways.

I can't solve it by using useCallback on my handler since I would have to pass it my objects as a dependency, which is itself recreated everytime an object's name changes.

It seems to me like it is not necessary for all the objects that haven't changed to rerender anyway because the handler changed. And there should be a way to improve this.

Any ideas ?

I've seen in the React Sortly repo that they use debounce in combination with each object editor changing it's own state. This allows only the edited component to change and rerender while someone is typing and updates the parent only once if no other change event comes up in a given delay.

handleChangeName = (e) => {
    this.setState({ name: e.target.value }, () => this.change());
  }

  change = debounce(() => {
    const { index, onChange } = this.props;
    const { name } = this.state;
    onChange(index, { name });
  }, 300);

This is the best solution I can see right now but since they use the setState callback function I haven't been able to figure out a way to make this work with hooks.

like image 452
Geoffrey H Avatar asked Sep 06 '19 12:09

Geoffrey H


People also ask

Does useCallback improve performance?

To conclude, useCallback and useMemo hooks aim to improve web performance with the help of the Memoization technique, but there's a slight difference between them; useCallback is used to memoize functions whereas useMemo is used to memoize values.

Does useCallback work without React memo?

So in this case, you should utilize useCallback for the parent component, and also utilize React. memo for the child component. However, if you want to utilize useCallback with useEffect, you can handle useCallback without React. memo.

When you should not use useMemo?

useMemo itself requires memory so if we are trying to over-optimize by memoizing every function, it might slow the application down. We should also not use useMemo when the function returns a primitive value, such as a boolean or a string.

What is the best method to update state in component React?

To update our state, we use this. setState() and pass in an object. This object will get merged with the current state. When the state has been updated, our component re-renders automatically.


1 Answers

You have to use the functional form of setState:

setState((prevState) => {
  // ACCESS prevState
  return someNewState;
});

You'll be able to access the current state value (prevState) while updating it.

Then way you can use the useCallback hook without the need of adding your state object to the dependency array. The setState function doesn't need to be in the dependency array, because it won't change accross renders.

Thus, you'll be able to use React.memo on the children, and only the ones that receive different props (shallow compare) will re-render.

EXAMPLE IN SNIPPET BELOW

const InputField = React.memo((props) => {
  console.log('Rendering InputField '+ props.index + '...');
  return(
    <div>
      <input 
        type='text' 
        value={props.value}
        onChange={()=>
          props.handleChange(event.target.value,props.index)
        }
      />  
    </div>
  );
});

function App() {
  
  console.log('Rendering App...');
  
  const [inputValues,setInputValues] = React.useState(
    ['0','1','2']
  );
  
  const handleChange = React.useCallback((newValue,index)=>{
    setInputValues((prevState)=>{
      const aux = Array.from(prevState);
      aux[index] = newValue;
      return aux;
    });
  },[]);
  
  const inputItems = inputValues.map((item,index) => 
    <InputField 
      value={item}
      index={index}
      handleChange={handleChange}
    />
  );

  return(
    <div>
      {inputItems}
    </div>
  );
}



ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
like image 139
cbdeveloper Avatar answered Oct 05 '22 13:10

cbdeveloper