Закрытие в результате перехвата `useState` вызывает раздражающее поведение

#javascript #reactjs #closures

#javascript #reactjs #закрытие

Вопрос:

У меня есть такой компонент:

 const MyInput = ({ defaultValue, id, onChange }) => {

  const [value, setValue] = useState(defaultValue);
  const handleChange = (e) => {
    const value = e.target.value;
    setValue(value);

    onChange(id, value);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
};
 

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

В моем родительском компоненте это работает нормально,

 export default function App() {
  const [formState, setFormState] = useState({});

  const handleChange = (id, value) => {
    console.log(id, value, JSON.stringify(formState)); // For debugging later
    setFormState({ ...formState, [id]: value });
  };


  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}
 

но проблема в том, что значение formState не отражает значения в MyInputs , пока они не запустят событие onchange.

Итак, хорошо, я могу просто добавить перехват useEffect ‘component did mount’ на myInput для запуска onChange при первом монтировании компонента.

 const MyInput = ({ defaultValue, id, onChange }) => {
  useEffect(() => {
    onChange(id, defaultValue);
  }, []);

  const [value, setValue] = useState(defaultValue);
  const handleChange = (e) => {
    const value = e.target.value;
    setValue(value);

    onChange(id, value);
  };

  return (
    <div>
      <input type="text" value={value} onChange={handleChange} />
    </div>
  );
};
 

Изолированная среда кода

Но это ведет себя не так, как мы хотим. Каждый из MyInputs вызывает onChange при первом монтировании, но ссылка на formState устаревает для основного обработчика onChange в каждом случае.

т.е. консоль.вывод журнала при первой загрузке страницы выглядит следующим образом:

 1 one {}
2 two {}
3 three {}
4 four {}
5 five {}
 

Поэтому я подумал, что использование ссылки может решить эту проблему. например:

 export default function App() {
  const [formState, setFormState] = useState({});

  const formStateRef = useRef(formState);

  useEffect(() => {
    formStateRef.current = formState;
  }, [formState]);

  const handleChange = (id, value) => {
    console.log(
      id,
      value,
      JSON.stringify(formState),
      JSON.stringify(formStateRef)
    );
    setFormState({ ...formStateRef.current, [id]: value });
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}
 

Изолированная среда кода

Это не работает. Похоже, что все обработчики изменений срабатывают до того, как useEffect для formState получит возможность обновить ссылку.

 1 one {} {"current":{}}
2 two {} {"current":{}}
3 three {} {"current":{}}
4 four {} {"current":{}}
5 five {} {"current":{}}
 

Я думаю, я мог бы напрямую изменить ссылку в функции handleChange и установить это?

 export default function App() {
  const [formState, setFormState] = useState({});

  const formStateRef = useRef(formState);

  const handleChange = (id, value) => {
    console.log(
      id,
      value,
      JSON.stringify(formState),
      JSON.stringify(formStateRef)
    );

    formStateRef.current = { ...formStateRef.current, [id]: value };
    setFormState(formStateRef.current);
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput id="1" defaultValue="one" onChange={handleChange} />
      <MyInput id="2" defaultValue="two" onChange={handleChange} />
      <MyInput id="3" defaultValue="three" onChange={handleChange} />
      <MyInput id="4" defaultValue="four" onChange={handleChange} />
      <MyInput id="5" defaultValue="five" onChange={handleChange} />
    </div>
  );
}

 

Изолированная среда кода

Это действительно работает. Но это использование ссылок вызывает у меня некоторое беспокойство.

Каков стандартный способ добиться чего-то подобного?

Комментарии:

1. Почему бы не передавать defaultValue и onChange не передавать непосредственно на базовые входные данные и не позволять обновлениям состояния в родительском элементе происходить нормально? Именно так в любом случае будет работать неконтролируемый ввод. Кажется, вы слишком усложняете ситуацию.

2. @DrewReese — вы имеете в виду, что именно так будут работать управляемые входные данные? — Мой фактический вариант использования сложнее, чем этот, он включает в себя динамическую визуализацию элементов формы из определения JSON. Для моего использования просто проще сохранить их как неконтролируемые компоненты.

3. Нет, управляемые входы используют value атрибут и управляются состоянием, неконтролируемые входы принимают defaultValue атрибут за начальное значение и не имеют никакого состояния, управляющего ими.

4. В любом случае, если вы возьмете свой первый пример и используете обновление функционального состояния, вы получите результат, который, я думаю, вы ожидаете: setFormState(formState => ({ ...formState, [id]: value })); . Вы пытаетесь заполнить свое состояние формы, когда входные данные монтируются, верно?

5. @DrewReese О, да — я не знал, что вы можете передать defaultValue реквизит базовым элементам. В любом случае — у меня все та же проблема. codesandbox.io/s/gifted-zhukovsky-6sdqu

Ответ №1:

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

Проблема

При нефункциональном обновлении все входные данные монтируются одновременно, и все они вызывают свои onChange обработчики. Обратный handleChange вызов использует состояние из цикла рендеринга, в который он был заключен. Когда все входные данные ставят обновления в очередь в одном и том же цикле рендеринга, каждое последующее обновление перезаписывает предыдущее обновление состояния. Последний ввод для обновления состояния — это тот, который «выигрывает». Вот почему вы видите:

 {
  "5": "five"
}
 

Вместо того , чтобы

 {
  "1": "one",
  "2": "two",
  "3": "three",
  "4": "four",
  "5": "five"
}
 

Решение

Используйте обновление функционального состояния для корректного обновления из предыдущего состояния, а не состояния из предыдущего цикла рендеринга.

 setFormState(formState => ({ ...formState, [id]: value }));
 

Отсюда предложения — это только оптимизация

  1. Карри входной идентификатор в обработчике, что позволяет передавать и обрабатывать на один реквизит меньше. Дочерним входным данным также не нужно знать это, чтобы обрабатывать изменения.
     const handleChange = (id) => (value) => {
      setFormState((formState) => ({ ...formState, [id]: value }));
    };
     
  2. Перейдите defaultValue к defaultValue реквизиту базовых входных данных.
     const MyInput = ({ defaultValue, onChange }) => {
      useEffect(() => {
        onChange(defaultValue);
      }, []);
    
      const handleChange = (e) => {
        const { value } = e.target;
        onChange(value);
      };
    
      return (
        <div>
          <input type="text" defaultValue={defaultValue} onChange={handleChange} />
        </div>
      );
    };
     
  3. Передайте id в обработчик карри
     <MyInput defaultValue="one" onChange={handleChange(1)} />
    <MyInput defaultValue="two" onChange={handleChange(2)} />
    <MyInput defaultValue="three" onChange={handleChange(3)} />
    <MyInput defaultValue="four" onChange={handleChange(4)} />
    <MyInput defaultValue="five" onChange={handleChange(5)} />
     

ДЕМОНСТРАЦИЯ

Редактировать закрытие в результате перехвата usestate -вызывает раздражающее поведение

демонстрационный код

 const MyInput = ({ defaultValue, onChange }) => {
  useEffect(() => {
    onChange(defaultValue);
  }, []);

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

  return (
    <div>
      <input type="text" defaultValue={defaultValue} onChange={handleChange} />
    </div>
  );
};

export default function App() {
  const [formState, setFormState] = useState({});

  const handleChange = (id) => (value) => {
    setFormState((formState) => ({ ...formState, [id]: value }));
  };

  return (
    <div className="App">
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <MyInput defaultValue="one" onChange={handleChange(1)} />
      <MyInput defaultValue="two" onChange={handleChange(2)} />
      <MyInput defaultValue="three" onChange={handleChange(3)} />
      <MyInput defaultValue="four" onChange={handleChange(4)} />
      <MyInput defaultValue="five" onChange={handleChange(5)} />
    </div>
  );
}
 

Комментарии:

1. Хм, не знал, что вы можете установить состояние из предыдущего состояния. Большое спасибо.

2. @dwjohnston Добро пожаловать. Официальные документы React docs — это потрясающий ресурс! 😀 Приветствия и удачи!