4

I am using React functional components alongside React.memo, but I am experiencing an issue, which in my opinion derives from JavaScript closures. (I am also using Immer for immutability, but I believe it does not affect the situation.)

Below is a simplified version of the situation:

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

I am using React.memo to avoid unnecessary rerenders of the ChildComponents whose value didn't change, but this way the ChildComponent is storing an old version of the onChange function, which (in my understanding due to JavaScript closures) references an old version of the data.

The result of this is that when I change initially the value of the first ChildComponent and then the value of another ChildComponent, the value of the first ChildComponent is restored to the initial value.

A reproduction of the situation can be found in this sandbox.

After searching the web, a solution that I found to this issue is to use a ref inside the onChange function, to get the up-to-date data, like this:

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

A sandbox with this solution can be found here.

This solves the issue. But I wanted to ask, is this a correct solution? Or am I using React and React Hooks the wrong way?

4

1 回答 1

2

Although using a reference is a valid solution, you should use a 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;
    })
  );
};

Edit sparkling-haze-e9y22

于 2019-11-22T11:55:44.727 回答