如何注册事件与 useEffect 挂钩?

我正在学习一门关于如何用挂钩注册事件的 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。当我试验结果时,奇怪的是,我键入的每个键,不是附加,而是覆盖。

我原以为 SetUserText (${userText}${key}) ;会在当前状态后添加新类型的键,并将其设置为新状态,但实际上它忘记了旧状态并用新状态重写。

我们应该在每次重新呈现时注册和注销事件,这真的是正确的方法吗?

167011 次浏览

在第二种方法中,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将总是绑定到带有参数 []的初始值 ''

你不需要这样做,只是想让你知道为什么你的第二个解决方案不工作。

您将需要一种方法来跟踪以前的状态。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>
);

Edit q98jov5kvq

您无法访问已更改的 useText 状态。你可以把它比喻成普林斯顿州。将状态存储在一个变量中,例如:

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>
);

问题

[ ... ]在每次重新渲染时,事件都会继续注册和注销,我只是不认为这是正确的方法。

你是对的。在 每个渲染时重新启动 useEffect内部的事件处理是没有意义的。

[ ... ]空数组作为第二个参数,让组件只运行一次效果[ ... ]奇怪的是,在我键入的每个键上,它被覆盖而不是追加。

这是 陈旧闭包值的一个问题。

原因: useEffect中使用的函数应该是 依赖关系的一部分。没有设置任何依赖项([]) ,但仍然调用 handleUserKeyPress,它本身读取 userText状态。

解决方案

更新 : React 开发人员提出了一个包含新的 useEvent Hook (名称可能会更改)的 RFC来解决这种具有依赖关系的确切类型的事件相关问题。

在那之前,根据你的用例还有其他的选择:

1. 状态更新功能

setUserText(prev => `${prev}${key}`);

最小创伤手术
只能访问自己以前的状态,而不能访问其他状态的 Hook

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/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; }); // update 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/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>

Something 可用于不会通过数据流触发重新呈现的回调/事件处理程序
Something 不需要管理依赖项
Something 更强制的方法
Something only 推荐作为 React docs 的最后一个选项

看看这些链接获得更多信息: 1 2 3

3. useReducer-“作弊模式”

我们可以切换到 useReducer并访问当前的状态/道具-使用类似于 useState的 API。

变体2a: 减速器函数中的逻辑

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/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: 逻辑外减速器功能-类似于 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>

Something 不需要管理依赖项
访问多个状态和道具
Something 与 useState相同的 API
Something 可扩展到更复杂的情况/减少程序
由于直列减速器(可以忽略不计) ,性能略有下降
还原剂的复杂度略有增加

不恰当的解决方案

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/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有着大致相同的问题,后者只是导致了更多的依赖关系的间接性。

换句话说: 我们没有通过 useCallback添加额外的依赖层,而是将函数直接放在 useEffect中——但是所有的依赖仍然需要设置,导致频繁的处理程序更改。

实际上,如果您将 handleUserKeyPress移到 useEffect中,ESLint 详尽的 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]); // 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/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>

接受的答案是工作,但如果您正在注册 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]);

下面是基于@ford04答案的 useRef 解决方案,并移动到自定义钩子。我最喜欢它,因为它不需要添加任何手动依赖项,并且允许隐藏自定义钩子中的所有复杂性。

const useEvent = (eventName, eventHandler) => {
const cbRef = useRef(eventHandler)


useEffect(() => {
cbRef.current = eventHandler
}) // update after each render


useEffect(() => {
console.log("+++ subscribe")
const cb = (e) => cbRef.current(e) // then use most recent cb value
window.addEventListener(eventName, cb)
return () => {
console.log("--- unsubscribe")
window.removeEventListener(eventName, cb)
}
}, [eventName])
return
}

应用程序中的用法:

function App() {
const [isUpperCase, setUpperCase] = useState(false)
const [userText, setUserText] = useState("")


const handleUserKeyPress = (event) => {
const { key, keyCode } = event
if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
const displayKey = isUpperCase ? key.toUpperCase() : key
const newText = userText + displayKey
setUserText(newText)
}
}
useEvent("keydown", handleUserKeyPress)


return (
<div>
<h1>Feel free to type!</h1>
<label>
<input
type="checkbox"
defaultChecked={isUpperCase}
onChange={() => setUpperCase(!isUpperCase)}
/>
Upper Case
</label>
<blockquote>{userText}</blockquote>
</div>
)
}


Edit