Я следую курсу Udemy о том, как регистрировать события с помощью хуков, инструктор дал следующий код:
const [userText, setUserText] = useState('');
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
});
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
Теперь это прекрасно работает, но я не уверен, что это правильный путь. Причина в том, если я правильно понимаю, при каждом повторном рендеринге события будут продолжать регистрироваться и отменять регистрацию каждый раз, и я просто не думаю, что это правильный способ сделать это.
Поэтому я сделал небольшую модификацию крючков useEffect ниже.
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
Имея пустой массив в качестве второго аргумента, позволяя компоненту запускать эффект только один раз, имитируя componentDidMount. И когда я пробую результат, странно, что при каждой клавише, которую я набираю, вместо добавления она перезаписывается.
Я ожидал, что установитьТекстПользователя(${userText}${key}); будет иметь новый типизированный ключ, добавленный к текущему состоянию и установленный как новое состояние, но вместо этого он забывает старое состояние и перезаписывает с новым состоянием.
Действительно ли это был правильный способ регистрации и отмены регистрации события при каждом повторном рендеринге?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Во втором подходе useEffect привязывается только один раз и, следовательно, userText никогда не обновляется. Один из подходов — поддерживать локальную переменную, которая обновляется вместе с объектом userText при каждом нажатии клавиши.
const [userText, setUserText] = useState('');
let local_text = userText
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
local_text = `${userText}${key}`;
setUserText(local_text);
}
};
useEffect(() => {
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
Лично мне не нравится решение, чувствуется anti-react, и я думаю, что первый метод достаточно хорош и предназначен для использования таким образом.
новый ответ:
useEffect(() => {
function handlekeydownEvent(event) {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prevUserText => `${prevUserText}${key}`);
}
}
document.addEventListener('keyup', handlekeydownEvent)
return () => {
document.removeEventListener('keyup', handlekeydownEvent)
}
}, [])
при использовании setUserText передайте функцию в качестве аргумента вместо объекта, prevUserText всегда будет самым новым состоянием.
старый ответ:
попробуйте это, он работает так же, как ваш исходный код:
useEffect(() => {
function handlekeydownEvent(event) {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
}
document.addEventListener('keyup', handlekeydownEvent)
return () => {
document.removeEventListener('keyup', handlekeydownEvent)
}
}, [userText])
потому что в вашем методе useEffect() это зависит от переменной userText, но вы не помещаете ее во второй аргумент, иначе userText всегда будет привязан к начальному значению '' с аргументом [].
вам не нужно делать это, просто хочу, чтобы вы знали, почему ваше второе решение не работает.
Добавление [userText] — это то же самое, что и без второго аргумента, верно? Причина в том, что у меня есть только userText в приведенном выше примере, и без второго аргумента просто означает повторную визуализацию при каждом изменении реквизита/состояния, я не понимаю, как это отвечает на мой вопрос. **P/S: ** Я не против, в любом случае спасибо за ответ
эй @Isaac, да, это то же самое, что и без второго аргумента, я просто хочу, чтобы вы знали, почему ваше второе решение не работает, потому что ваше второе решение useEffect() зависит от переменной userText, но вы не указали вторые аргументы .
Но добавление [userText] также означает регистрацию и отмену регистрации события при каждом повторном рендеринге, верно?
точно! вот почему я говорю, что это то же самое с вашим первым решением.
ха-ха, да, я понял идею, но дело в том, что я чувствую, что это неэффективно, потому что я чувствую, что событие не должно быть register и deregister при каждом повторном рендеринге, а вместо этого должно быть как классические componentDidMount и componentWillUnmount, где они только зарегистрируйтесь один раз, если вы поняли мою точку зрения?
Хотя ваша текущая рекомендация — зарегистрироваться на мероприятии componentDidUpdate, что не так, как мы все сделали правильно?
понял, что вы имеете в виду, если вы действительно хотите зарегистрировать его только один раз в этом примере, вам нужно использовать useRef, как ответ @Maaz Syed Adeeb.
Я только что проголосовал за ваш ответ за то, что вы прошли через это со мной. Интересно, исходя из вашего понимания, является ли useRef более рекомендуемым способом по сравнению с ответом di3 выше?
@ Исаак, в моем понимании, да.
эй, @Исаак, теперь у меня новое понимание, тебе не нужны ни useRef, ни useCallback, только useState и useEffect, см. мой новый ответ.
Вам понадобится способ отслеживать предыдущее состояние. useState помогает отслеживать только текущее состояние. Из документы есть способ получить доступ к старому состоянию, используя другой хук.
const prevRef = useRef();
useEffect(() => {
prevRef.current = userText;
});
Я обновил ваш пример, чтобы использовать это. И это работает.
const { useState, useEffect, useRef } = React;
const App = () => {
const [userText, setUserText] = useState("");
const prevRef = useRef();
useEffect(() => {
prevRef.current = userText;
});
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${prevRef.current}${key}`);
}
};
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src = "https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id = "root"></div>Для вашего варианта использования useEffect нужен массив зависимостей для отслеживания изменений, и на основе зависимости он может определить, следует ли повторно отображать или нет. Всегда рекомендуется передавать массив зависимостей в useEffect. Пожалуйста, посмотрите код ниже:
Я представил useCallback крючок.
const { useCallback, useState, useEffect } = React;
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prevUserText => `${prevUserText}${key}`);
}
}, []);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]);
return (
<div>
<blockquote>{userText}</blockquote>
</div>
);
Я пробовал ваше решение, но оно точно такое же, как [userText] или без второго аргумента. По сути, мы помещаем console.info внутри useEffect, и мы увидим, что журналирование запускает каждый повторный рендеринг, что также означает, что addEventListender запускается при каждом повторном рендеринге.
Я хочу верить, что это ожидаемое поведение. Я обновил свой ответ.
В песочнице вы поместили оператор console.info('>'); в хуки useEffect, и, используя ваш обновленный код, он по-прежнему регистрируется каждый раз, что также означает, что события по-прежнему регистрируются при каждом повторном рендеринге.
Нет, для addEventListener это так не работает. Он не вызывается несколько раз. developer.mozilla.org/en-US/docs/Web/API/EventTarget/…
но из-за return () => {window.removeEventListener('keydown', handleUserKeyPress)} при каждом повторном рендеринге компонент будет регистрироваться и отменять регистрацию
Мы можем поместить внутрь еще один консольный журнал return () => { console.info('im removing!')} и увидеть, что при каждом нажатии клавиши событие будет отменяться и перерегистрироваться.
Именно то поведение, которое я хотел, но вы можете наблюдать его @ codeandbox.io/s/n5j7qy051j
@Isaac, он не будет перерегистрироваться при каждом оказывать, только при каждом изменении userText (то есть при большинстве, но не при всех нажатиях клавиш)
@Aprillion: я понимаю вашу точку зрения, но не думаю, что это решение — то, что я действительно ищу. Вместо этого вы можете взглянуть на принятый ответ
у вас нет доступа к измененному состоянию us eText. вы можете сравнить его с prevState. сохранить состояние в переменной, например: состояние следующим образом:
const App = () => {
const [userText, setUserText] = useState('');
useEffect(() => {
let state = ''
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
state += `${key}`
setUserText(state);
}
};
window.addEventListener('keydown', handleUserKeyPress);
return () => {
window.removeEventListener('keydown', handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
};
Лучший способ справиться с такими сценариями — посмотреть, что вы делаете в обработчике событий.
Если вы просто устанавливаете state, используя предыдущий state, лучше всего использовать шаблон обратного вызова и регистрировать прослушиватели событий только на монтировании исходный.
Если вы не используете шаблон обратного вызова, ссылка прослушивателя вместе с его лексической областью действия используется прослушивателем событий, но создается новая функция с обновленным закрытием при каждом рендеринге; следовательно, в обработчике вы не сможете получить доступ к обновленному состоянию
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)){
setUserText(prevUserText => `${prevUserText}${key}`);
}
}, []);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
но это создаст новую связанную функцию при каждом нажатии клавиши. если вы сосредоточены на производительности, локальная переменная состояния намного лучше
@di3 Если вы используете useCallback для определения функции с пустым массивом зависимостей, у вас также не будет этой проблемы.
если handleUserKeyPress не втягивается внутрь useEffect, то это внешняя зависимость и должна быть явной: useEffect(..., [handleUserKeyPress])
@Aprillion, это неправда, пустой массив специально используется для проверки монтирования/размонтирования. намерение состоит в том, чтобы вызвать его только один раз.
если намерение состоит в том, чтобы вызвать ее один раз, то не создавайте новую неиспользуемую функцию при каждом рендеринге и извлекайте ее из useEffect. ваше решение создаст ошибку, если вы добавите зависимости ко второму аргументу useCallback, потому что в useEffect отсутствует собственная зависимость
Я вижу, что важно использовать prevUserText для ссылки на предыдущее состояние userText. что, если мне нужно получить доступ к нескольким состояниям? Как я могу получить доступ ко всему предыдущему состоянию?
Как мы можем получить предыдущее состояние, если мы используем useReducer вместо useState?
это может изменить состояние, используя предыдущее состояние, но как вы сможете получить доступ к текущему состоянию?
@JeremyNelson, если под доступом к текущему состоянию вы подразумеваете доступ к обновленному состоянию, вы можете сделать это либо с помощью нового useEffect, либо, поскольку у вас уже есть значение, которое нужно вернуть из setUserText, вы можете использовать его перед возвратом из его обратного вызова.
@ShubhamKhatri, это именно то, что я искал! <3 Может ли кто-нибудь, разбирающийся в хуках, объяснить в нескольких словах, когда передавать параметр самому эффекту (первый аргумент useEffect), а когда нет? Кажется, что это работает нормально в обоих случаях, когда я регистрирую обновление состояния в манере didupdate, но мне не ясно, подходит ли это для более сложных случаев.
@tractatusviii Думаю, этот пост вам поможет: stackoverflow.com/questions/55840294/…
@Eesa У вас всегда есть предыдущее состояние, доступное вам в функции редуктора, предоставленной useReducer. Функция обратного вызова prevState — это обходной путь для useState.
У меня была такая же проблема, и я долго с ней боролся. но ваше решение спасло мой день :)
[...] on each and every re-render, events will keep registering and deregistering every time and I simply don't think it is the right way to go about it.
Ты прав. Нет смысла перезапускать обработку событий внутри useEffect на рендере каждый.
[...] empty array as the second argument, letting the component to only run the effect once [...] it's weird that on every key I type, instead of appending, it's overwritten instead.
Это проблема с устаревшие значения закрытия.
Причина: Используемые функции внутри useEffect должны быть часть зависимостей. Вы ничего не устанавливаете в качестве зависимости ([]), но все равно вызываете handleUserKeyPress, который сам считывает состояние userText.
Есть несколько альтернатив в зависимости от вашего варианта использования.
setUserText(prev => `${prev}${key}`);
✔ наименее инвазивный подход
✖ доступ только к собственному предыдущему состоянию, а не к другим состояниям
const App = () => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(prev => `${prev}${key}`); // use updater function here
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []); // still no dependencies
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>useReducer - "мошеннический режим"Мы можем переключиться на useReducer и получить доступ к текущему состоянию/реквизиту — с API, аналогичным useState.
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
// isUpperCase is always the most recent state (no stale closure value)
return `${state}${isUpperCase ? key.toUpperCase() : key}`;
}, "");
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
// isUpperCase is always the most recent state (no stale closure)
return `${state}${isUpperCase ? key.toUpperCase() : key}`;
}
}, "");
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style = {{ width: "150px" }} onClick = {() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>useState функции обновленияconst [userText, setUserText] = useReducer((state, action) =>
typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);
const App = () => {
const [isUpperCase, setUpperCase] = useState(false);
const [userText, setUserText] = useReducer(
(state, action) =>
typeof action === "function" ? action(state, isUpperCase) : action,
""
);
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(
(prevState, isUpper) =>
`${prevState}${isUpper ? key.toUpperCase() : key}`
);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
<button style = {{ width: "150px" }} onClick = {() => setUpperCase(b => !b)}>
{isUpperCase ? "Disable" : "Enable"} Upper Case
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>✔ нет необходимости управлять зависимостями
✔ доступ к нескольким состояниям и реквизиту
✔ тот же API, что и у useState
✔ расширяется до более сложных случаев/редукторов
✖ немного меньшая производительность из-за встроенного редуктора (своего рода пренебрежительный)
✖ немного увеличена сложность редуктора
useRef / сохранить обратный вызов в изменяемой ссылкеconst cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after each render
useEffect(() => {
const cb = e => cbRef.current(e); // then use most recent cb value
window.addEventListener("keydown", cb);
return () => { window.removeEventListener("keydown", cb) };
}, []);
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
const cbRef = useRef(handleUserKeyPress);
useEffect(() => {
cbRef.current = handleUserKeyPress;
});
useEffect(() => {
const cb = e => cbRef.current(e);
window.addEventListener("keydown", cb);
return () => {
window.removeEventListener("keydown", cb);
};
}, []);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>✔ для обратных вызовов/обработчиков событий, не используемых для повторного рендеринга потока данных
✔ нет необходимости управлять зависимостями
✖ рекомендуется только как последний вариант в документации React
✖ более императивный подход
Взгляните на эти ссылки для получения дополнительной информации: 123
useCallbackХотя его можно применять по-разному, useCallback не подходит для этого конкретного случая вопроса.
Причина: из-за добавленных зависимостей — userText здесь — прослушиватель событий будет перезапущен при нажатии клавиши каждый, в лучшем случае не будет работать или, что еще хуже, вызовет несоответствия.
const App = () => {
const [userText, setUserText] = useState("");
const handleUserKeyPress = useCallback(
event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
},
[userText]
);
useEffect(() => {
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [handleUserKeyPress]); // we rely directly on handler, indirectly on userText
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>Для полноты картины, вот несколько ключевых моментов по useCallback в целом:
✔ универсальное практичное решение
✔ минимально инвазивный
✖ ручное управление зависимостями
✖ useCallback делает определение функции более подробным/загроможденным
useEffectОбъявление события функция-обработчик прямо внутри useEffect имеет более или менее те же проблемы, что и useCallback, последнее просто вызывает немного большую косвенность зависимостей.
Другими словами: вместо того, чтобы добавлять дополнительный уровень зависимостей через useCallback, мы помещаем функцию непосредственно внутрь useEffect, но все зависимости все еще необходимо установить, что приводит к частым изменениям обработчика.
На самом деле, если вы переместите handleUserKeyPress внутрь useEffect, исчерпывающее правило отложений ESLint сообщит вам, какие именно канонические зависимости отсутствуют (userText), если не указано иное.
const App =() => {
const [userText, setUserText] = useState("");
useEffect(() => {
const handleUserKeyPress = event => {
const { key, keyCode } = event;
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
setUserText(`${userText}${key}`);
}
};
window.addEventListener("keydown", handleUserKeyPress);
return () => {
window.removeEventListener("keydown", handleUserKeyPress);
};
}, [userText]); // ESLint will yell here, if `userText` is missing
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));<script src = "https://unpkg.com/[email protected]/umd/react.production.min.js"></script>
<script src = "https://unpkg.com/[email protected]/umd/react-dom.production.min.js"></script>
<div id = "root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>Это начинает все больше и больше походить на пропущенный недостаток дизайна хуков.
Принятый ответ работает, но если вы регистрируете событие BackHandler, убедитесь, что return true в вашей функции handleBackPress, например:
const handleBackPress= useCallback(() => {
// do some action and return true or if you do not
// want the user to go back, return false instead
return true;
}, []);
useEffect(() => {
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [handleBackPress]);
Не могли бы вы включить некоторый код, чтобы продемонстрировать, как достичь моей цели во втором методе?