#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 }));
Отсюда предложения — это только оптимизация
- Карри входной идентификатор в обработчике, что позволяет передавать и обрабатывать на один реквизит меньше. Дочерним входным данным также не нужно знать это, чтобы обрабатывать изменения.
const handleChange = (id) => (value) => { setFormState((formState) => ({ ...formState, [id]: value })); };
- Перейдите
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> ); };
- Передайте
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)} />
ДЕМОНСТРАЦИЯ
демонстрационный код
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 — это потрясающий ресурс! 😀 Приветствия и удачи!