#javascript #reactjs #react-hooks
#javascript #reactjs #реагирующие перехваты
Вопрос:
Мне трудно понять, почему обработчик события onClick (который вызывает 2 вызова функции-оболочки пользовательского хука) не отвечает должным образом. Я ожидаю, что каждый раз, когда я нажимаю кнопку в примере, цвет границы будет меняться с зеленого на красный в зависимости от значения, которое увеличивается. Я понимаю, что пример является элементарным и может быть легко решен путем привязки значения ошибки к значению.значение вместо совместного использования, но это упрощенный пример более сложного взаимодействия, и я свел проблему к простому примеру для пояснения. Любая помощь будет оценена. https://codesandbox.io/s/custom-hooks-with-closure-issue-2fc6g?file=/index.js
index.js
import useValueErrorPair from "./useValueErrorPair";
import styled from "styled-components";
import ReactDOM from "react-dom";
import React from "react";
const Button = styled.button`
background-color: black;
padding: 10px;
color: white;
${props =>
props.error ? "border: 3px solid #ff0000;" : "border: 3px solid #00ff00;"}
`;
const e = React.createElement;
const DemoComponent = () => {
const [value, setValue, setError] = useValueErrorPair(0, false);
console.log(value);
return (
<Button
error={value.error}
onClick={e => {
e.preventDefault();
setError((value.value 1) % 2 === 1); // If number of clicks is odd => error.
setValue(value.value 1); // Increment the state hook for value.
}}
>
Click Me For Problems!
</Button>
);
};
const domContainer = document.querySelector("#root");
ReactDOM.render(e(DemoComponent), domContainer);
export default DemoComponent;
useValueErrorPair.js
import { useState } from "react";
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const setValue = newValue => {
setV({ error: v.error, value: newValue });
};
const setError = newError => {
if (newError !== v.error) setV({ error: newError, value: v.value });
};
return [v, setValue, setError];
};
export default useValueErrorPair;
Фрагмент:
const { useState } = React;
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const setValue = newValue => {
setV({ error: v.error, value: newValue });
};
const setError = newError => {
if (newError !== v.error) setV({ error: newError, value: v.value });
};
return [v, setValue, setError];
};
const DemoComponent = () => {
const [value, setValue, setError] = useValueErrorPair(0, false);
console.log(value);
return (
<button type="button" className={value.error ? "error" : "okay"}
onClick={e => {
e.preventDefault();
setError((value.value 1) % 2 === 1); // If number of clicks is odd => error.
setValue(value.value 1); // Increment the state hook for value.
}}
>
Click Me For Problems!
</button>
);
};
const domContainer = document.querySelector("#root");
const e = React.createElement;
ReactDOM.render(e(DemoComponent), domContainer);
.error {
border: 1px solid red;
}
.okay {
border: 1px solid green;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Комментарии:
1. Не используйте два «файла», создайте единый комбинированный фрагмент со всеми ненужными (например,
styled-components
) удаленными, чтобы мы могли сосредоточиться на реальной проблеме (хук).2. Я сделал это для вас, используя код из вопроса.
3. Извините, мой ответ был неверным вкратце (я пропустил часть необходимых изменений
setError
). Теперь это исправлено.4. И спасибо за редактирование кода в вопросе!
5. Спасибо, что исправили это. Я отвлекся на собрание и отказался от этого на несколько часов.
Ответ №1:
Проблема в том, что ваши функции установки используют устаревшее состояние. При настройке нового состояния на основе существующего состояния вам следует использовать форму обратного вызова, чтобы вы всегда имели дело с актуальной информацией. В вашем случае вызов setError
работал нормально, но затем вызов setValue
использовал устаревшую копию v
и отменял setError
внесенные изменения.
Если мы используем форму обратного вызова, проблема исчезает, см. ***
Комментарии:
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const setValue = newValue => {
// *** Use the callback form when setting state based on existing state
setV(({error}) => ({error, value: newValue}));
};
const setError = newError => {
// *** Again
setV(prev => {
if (newError !== prev.error) {
return { error: newError, value: prev.value };
}
// No change
return prev;
});
};
return [v, setValue, setError];
};
const { useState } = React;
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const setValue = newValue => {
// *** Use the callback form when setting state based on existing state
setV(({error}) => ({error, value: newValue}));
};
const setError = newError => {
// *** Again
setV(prev => {
if (newError !== prev.error) {
return { error: newError, value: prev.value };
}
// No change
return prev;
});
};
return [v, setValue, setError];
};
const DemoComponent = () => {
const [value, setValue, setError] = useValueErrorPair(0, false);
console.log(value);
return (
<button type="button" className={value.error ? "error" : "okay"}
onClick={e => {
e.preventDefault();
setError((value.value 1) % 2 === 1); // If number of clicks is odd => error.
setValue(value.value 1); // Increment the state hook for value.
}}
>
Click Me, It's Working!
</button>
);
};
const domContainer = document.querySelector("#root");
const e = React.createElement;
ReactDOM.render(e(DemoComponent), domContainer);
.error {
border: 1px solid red;
}
.okay {
border: 1px solid green;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
В этом есть еще одно преимущество: Вы можете сделать установочные функции стабильными, как те, которые вы получаете useState
, вместо того, чтобы воссоздавать их каждый раз (что может иметь побочные эффекты, приводящие к повторному рендерингу компонентов без необходимости). Для хуков я предпочитаю использовать ссылки для стабильности, а не useMemo
(или useCallback
, который использует useMemo
), поскольку в useMemo
документах говорится, что это не семантическая гарантия. (Это также уменьшает количество функций, которые вы создаете и выбрасываете.)
Вот как это будет выглядеть:
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const settersRef = useRef(null);
if (!settersRef.current) {
settersRef.current = {
setValue: newValue => {
setV(({error}) => ({error, value: newValue}));
},
setError: newError => {
setV(prev => {
if (newError !== prev.error) {
// Update
return { error: newError, value: prev.value };
}
// No change
return prev;
});
},
};
}
return [v, settersRef.current.setValue, settersRef.current.setError];
};
Живой пример:
const { useState, useRef } = React;
const useValueErrorPair = (initialValue, initialError) => {
const [v, setV] = useState({ value: initialValue, error: initialError });
const settersRef = useRef(null);
if (!settersRef.current) {
settersRef.current = {
setValue: newValue => {
setV(({error}) => ({error, value: newValue}));
},
setError: newError => {
setV(prev => {
if (newError !== prev.error) {
// Update
return { error: newError, value: prev.value };
}
// No change
return prev;
});
},
};
}
return [v, settersRef.current.setValue, settersRef.current.setError];
};
const DemoComponent = () => {
const [value, setValue, setError] = useValueErrorPair(0, false);
console.log(value);
return (
<button type="button" className={value.error ? "error" : "okay"}
onClick={e => {
e.preventDefault();
setError((value.value 1) % 2 === 1); // If number of clicks is odd => error.
setValue(value.value 1); // Increment the state hook for value.
}}
>
Click Me, It's Working!
</button>
);
};
const domContainer = document.querySelector("#root");
const e = React.createElement;
ReactDOM.render(e(DemoComponent), domContainer);
.error {
border: 1px solid red;
}
.okay {
border: 1px solid green;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Комментарии:
1. Это отличный материал. Под стабильным вы подразумеваете, что будет только одна ссылка на функции setValue и setError, а не новые экземпляры этих функций, создаваемые каждый раз, когда другой компонент вызывает пользовательский хук useValueErrorPair? Я думаю, это также работает, потому что вы используете функциональную форму setState части хука useState (которую, я признаю, я раньше не видел).
2. @CodeoftheWarrior — Да, точно, как установщик из
useState
: «Примечание: React гарантирует, что идентификатор функции setState стабилен и не изменится при повторном рендеринге». Нам все равно нужно использовать форму обратного вызова (из-за проблемы устаревшего состояния), и это также позволяет это сделать. Счастливого кодирования!