当回调更改父级中的状态时,如何使用React.memo和useCallback优化React组件

时间:2019-09-06 12:34:00

标签: reactjs react-hooks

我遇到了一个性能优化问题,我认为可以以某种方式解决该问题,但是我不确定如何解决。

假设我有一个想要编辑的对象集合。父组件包含所有对象,并使用编辑器组件呈现列表,该编辑器组件显示值并允许修改对象。

一个简单的例子是这样:

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

所以我可以使用React.memo,这样当我编辑一个对象的名称时,其他对象就不会重新呈现。但是,由于onChange的处理程序每​​次都在ObjectEditor的父组件中重新创建,因此所有对象始终始终呈现。

我无法通过在处理程序上使用useCallback来解决它,因为我必须将objects作为依赖项传递给它,它每次对象名称更改时都会重新创建。

在我看来,因为处理程序已更改,所有未更改的对象都不必重新渲染。并且应该有一种方法可以改善这一点。

有什么想法吗?

我在React Sortly回购中看到,他们结合使用debounce和每个对象编辑器来更改其自身的状态。 这样,当有人键入内容时,只有经过编辑的组件才能更改和重新呈现,并且如果在给定的延迟内没有其他更改事件发生,则仅更新一次父组件。

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);

这是我现在可以看到的最好的解决方案,但是由于它们使用了setState回调函数,所以我一直无法找到一种方法来使用钩子。

2 个答案:

答案 0 :(得分:1)

您必须使用setState的功能形式:

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

您可以在更新时访问当前状态值(prevState)。

然后,您可以使用useCallback挂钩,而无需将状态对象添加到依赖项数组中。 setState函数不必位于依赖项数组中,因为它不会在整个渲染过程中发生变化。

因此,您将能够在子级上使用React.memo,并且只有那些收到不同道具(浅比较)的子级才会重新渲染。

下面的片段示例

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"/>

答案 1 :(得分:0)

好的,因此,debounce如果包裹在useCallback中似乎可以使用 不知道为什么在newObject函数中似乎不必通过updateParent作为依赖项。

因此要进行这项工作,我必须进行以下更改:

首先,在父级中useCallback并将其更改为采用整个对象,而不是负责更新密钥。 然后更新ObjectEditor使其具有自己的状态并处理对键的更改。 并包装onChange处理程序,它将在debounce

中更新父项
import React, { useState, useEffect } from 'react'
import debounce from 'lodash.debounce'

const Input = props => {

    const { value, onChange } = props

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

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

const ObjectEditor = React.memo(props => {
    const { initialObject, onChange } = props

    const [object, setObject] = useState(initialObject)

    const updateParent = useCallback(debounce((newObject) => {
        onChange(newObject)
    }, 500), [onChange])

    // synchronize the object if it's changed in the parent
    useEffect(() => {
        setObject(initialObject)
    }, [initialObject])

    const handleChange = key => value => {
        const newObject = {
            ...object,
            [key]: value
        }
        setObject(newObject)
        updateParent(newObject)
    }

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

const Objects = props => {

    const { initialObjects } = props

    const [objects, setObjects] = useState(initialObjects)

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

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

export default Objects