如何解决在 React.memo 中使用 React 功能组件时的闭包问题?

How to solve closures issue when using React functional component with React.memo?

我将 React 功能组件与 React.memo 一起使用,但我遇到了一个问题,在我看来,该问题源于 JavaScript 闭包。 (我也使用 Immer 来实现不变性,但我相信这不会影响情况。)

以下是该情况的简化版本:

import React, { useState } from 'react';
import produce from "immer";

const initialData = [
  {id: 1, value: 0},
  {id: 2, value: 0},
  {id: 3, value: 0}
]

const ChildComponent = memo(
  ({ value, onChange }) => (
    <p>
      <input value={value} onChange={onChange} type="number" />
    </p>
  ),
  (prevProps, nextProps) => prevProps.value === nextProps.value
);

const ParentComponent = props => {
  const [data, setData] = useState(initialData);

  const onDataChange = id => event => {
    setData(
      produce(data, draft => {
        const record = draft.find(entry => entry.id === id);
        record.value = event.target.value;
      })
    );
  };

  return data.map(record => (
    <ChildComponent
      key={record.id}
      value={record.value}
      onChange={onDataChange(record.id)}
    />
  ));
};

我正在使用 React.memo 来避免不必要地重新呈现其值未更改的 ChildComponents,但这样 ChildComponent 存储的是旧版本的 onChange 函数,(据我所知,由于 JavaScript closures) 引用旧版本的数据。

这样做的结果是,当我最初更改第一个 ChildComponent 的值然后更改另一个 ChildComponent 的值时,第一个 ChildComponent 的值恢复为初始值。

可以在 this sandbox 中找到该情况的再现。

在网上搜索后,我找到了一个解决这个问题的方法是在 onChange 函数中使用 ref 来获取最新数据,如下所示:

const ParentComponent = props => {
  const [data, setData] = useState(initialData);
  const dataRef = useRef();
  dataRef.current = data;

  const onDataChange = id => event => {
    setData(
      produce(dataRef.current, draft => {
        const record = draft.find(entry => entry.id === id);
        record.value = event.target.value;
      })
    );
  };

  return data.map(record => (
    <ChildComponent
      key={record.id}
      value={record.value}
      onChange={onDataChange(record.id)}
    />
  ));
};

可以找到包含此解决方案的沙箱 here

这解决了问题。但我想问一下,这是一个正确的解决方案吗?还是我错误地使用了 React 和 React Hooks?

虽然使用引用是一个有效的解决方案,但您应该使用 functional setState

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

const onDataChange = id => event => {
  event.persist();
  setData(data =>
    produce(data, draft => {
      const record = draft.find(entry => entry.id === id);
      record.value = event.target.value;
    })
  );
};