Реагируйте эффективно обновлять объект в массиве с помощью ловушки useState - PullRequest
5 голосов
/ 22 апреля 2019

У меня есть компонент React, который отображает умеренно большой список входных данных (более 100 элементов). Он хорошо воспроизводится на моем компьютере, но на моем телефоне заметна задержка ввода. React DevTools показывает, что весь родительский объект перерисовывается при каждом нажатии клавиши.

Есть ли более эффективный способ подойти к этому?

https://codepen.io/anon/pen/YMvoyy?editors=0011

function MyInput({obj, onChange}) {
  return (
    <div>
      <label>
        {obj.label}
        <input type="text" value={obj.value} onChange={onChange} />
      </label>
    </div>
  );
}

// Passed in from a parent component
const startingObjects = 
  new Array(100).fill(null).map((_, i) => ({label: i, value: 'value'}));

function App() {
  const [objs, setObjects] = React.useState(startingObjects);
  function handleChange(obj) {
    return (event) => setObjects(objs.map((o) => {
      if (o === obj) return {...obj, value: event.target.value}
      return o;
    }));
  }
  return (
    <div>
      {objs.map((obj) => <MyInput obj={obj} onChange={handleChange(obj)} />)}  
    </div>
  );
}


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

1 Ответ

8 голосов
/ 22 апреля 2019

Проблема связана с:

function handleChange(obj) {
  return (event) => setObjects(objs.map((o) => {
    if (o === obj) return {...obj, value: event.target.value}
    return o;
  }));
}

В этом примере вы обновите массив objs.Это, конечно, хорошо, но React не знает, что изменилось, поэтому сработал Render на всех дочерних элементах.

Если ваш функциональный компонент отображает один и тот же результат с теми же параметрами, вы можете заключить его в вызовReact.memo для повышения производительности.

https://reactjs.org/docs/react-api.html#reactmemo

const MyInput = React.memo(({obj, onChange}) => {
    console.log(`Rerendered: ${obj.label}`);
    return <div style={{display: 'flex'}}>
        <label>{obj.label}</label>
        <input type="text" value={obj.value} onChange={onChange} />
    </div>;
}, (prevProps, nextProps) => prevProps.obj.label === nextProps.obj.label && prevProps.obj.value === nextProps.obj.value);

Однако React.Memo делает поверхностное сравнение только при попытке выяснить, должен ли он отображаться, поэтому мы можем передатьпользовательская функция сравнения в качестве второго аргумента.

(prevProps, nextProps) => prevProps.obj.label === nextProps.obj.label && prevProps.obj.value === nextProps.obj.value);

Говоря в основном, если метка и значение в obj prop такие же, как и предыдущие атрибуты предыдущего obj prop,не перерисовывать.

Наконец, setObjects так же, как setState, также является асинхронным и не будет немедленно отражать и обновлять.Таким образом, чтобы избежать риска некорректности objs и использования более старых значений, вы можете изменить это на обратный вызов, например:

function handleChange(obj) {
  return (event) => {
    const value = event.target.value;
    setObjects(prevObjs => (prevObjs.map((o) => {
      if (o === obj) return {...obj, value }
      return o;
    })))
  };
}

https://codepen.io/anon/pen/QPBLwy?editors=0011 имеет все это, а такжеconsole.logs, показывающий, если что-то перерисовано.


Есть ли более эффективный способ приблизиться к этому?

Вы храните все свои значения в массиве,это означает, что вы не знаете, какой элемент нужно обновить, не просматривая весь массив, сравнивая, соответствует ли объект.

Если вы начали с Объекта:

const startingObjects = 
  new Array(100).fill(null).reduce((objects, _, index) => ({...objects, [index]: {value: 'value', label: index}}), {})

Через некоторое времяВ модификациях ваша функция дескриптора изменится на

function handleChange(obj, index) {
  return (event) => {
    const value = event.target.value;
    setObjects(prevObjs => ({...prevObjs, [index]: {...obj, value}}));
  }
}

https://codepen.io/anon/pen/LvBPEB?editors=0011 в качестве примера.

...