问题
[...] 在每次重新渲染时,事件将继续注册和注销,我认为这不是正确的做法。
你是对的。在 useEffect
中在每个渲染中重新启动事件处理没有意义。
[...] 空数组作为第二个参数,让组件只运行一次效果 [...] 每次我键入键时,它都被覆盖了,而不是添加。
这是 闭包陈旧值 的问题。
原因:在 useEffect
中使用的函数应该是 依赖项的一部分。你将什么也没有设置为依赖项 ([]
),但仍然调用了 handleUserKeyPress
,它本身读取 userText
状态。
解决方案
0. useEffectEvent
(beta)
更新:React 开发人员提出了一个 RFC,其中包括新的 useEvent
Hook(名称和功能略有改变),以解决此类与依赖项相关的事件问题。
在那之前,根据您的用例,有不同的替代方案:
1. 状态更新函数
setUserText(prev => `${prev}${key}`)
✔ 最小侵入式方法
✖ 只能访问自己的先前状态,而不能访问其他状态的Hooks
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}`);
}
};
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://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
2. useRef
/ 可变引用
const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; });
useEffect(() => {
const cb = e => cbRef.current(e);
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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
✔ 可用于不通过数据流触发重新渲染的回调/事件处理程序
✔ 不需要管理依赖关系
✖ 更加命令式的方法
✖ React 文档只推荐作为最后的选择
请查看以下链接以获取更多信息: 1 2 3
3. useReducer
- "作弊模式"
我们可以切换到 useReducer
并访问当前状态/props - 具有与 useState
相似的 API。
变体 2a: reducer 函数内的逻辑
const [userText, handleUserKeyPress] = useReducer((state, event) => {
const { key, keyCode } = event;
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)) {
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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
变体2b:在reducer函数外部处理逻辑 - 类似于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/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
✔ 无需管理依赖项
✔ 访问多个状态和属性
✔ 与useState
相同的API
✔ 可扩展到更复杂的情况/减速器
✖ 内联减速器导致性能略低(有点可以忽略)
✖ 减速器复杂度稍有增加
不适当的解决方案
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]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
在useEffect
内部声明处理程序函数
直接在useEffect
内部声明事件处理程序函数与useCallback
存在更多或更少相同的问题,后者只会导致依赖关系略微间接。
换句话说:我们将函数直接放置在useEffect
中,而不是通过useCallback
添加额外的依赖层 - 但仍然需要设置所有依赖项,导致处理程序经常发生变化。
实际上,如果将handleUserKeyPress
移动到useEffect
内部,则ESLint exhaustive deps规则将告诉您缺少哪些确切的规范依赖项(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]);
return (
<div>
<h1>Feel free to type!</h1>
<blockquote>{userText}</blockquote>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
useCallback
来定义一个带有空依赖数组的函数,那么您也不会遇到这个问题。 - Shubham KhatriprevUserText
来引用userText
之前的状态。如果我需要访问多个状态怎么办?我如何访问所有先前的状态? - Joey Yi ZhaouseReducer
而不是useState
,我们如何获取先前的状态? - Eesa