Hook 实现基于 Tanner Linsley在 JSConf Hawaii 2020 上的精彩演讲:
useOuterClick
应用程序编程接口
const Client = () => {
const innerRef = useOuterClick(ev => {/*event handler code on outer click*/});
return <div ref={innerRef}> Inside </div>
};
执行
function useOuterClick(callback) {
const callbackRef = useRef(); // initialize mutable ref, which stores callback
const innerRef = useRef(); // returned to client, who marks "border" element
// update cb on each render, so second useEffect has access to current value
useEffect(() => { callbackRef.current = callback; });
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
function handleClick(e) {
if (innerRef.current && callbackRef.current &&
!innerRef.current.contains(e.target)
) callbackRef.current(e);
}
}, []); // no dependencies -> stable click listener
return innerRef; // convenience for client (doesn't need to init ref himself)
}
这是一个工作示例:
显示代码片段
/*
Custom Hook
*/
function useOuterClick(callback) {
const innerRef = useRef();
const callbackRef = useRef();
// set current callback in ref, before second useEffect uses it
useEffect(() => { // useEffect wrapper to be safe for concurrent mode
callbackRef.current = callback;
});
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
// read most recent callback and innerRef dom node from refs
function handleClick(e) {
if (
innerRef.current &&
callbackRef.current &&
!innerRef.current.contains(e.target)
) {
callbackRef.current(e);
}
}
}, []); // no need for callback + innerRef dep
return innerRef; // return ref; client can omit `useRef`
}
/*
Usage
*/
const Client = () => {
const [counter, setCounter] = useState(0);
const innerRef = useOuterClick(e => {
// counter state is up-to-date, when handler is called
alert(`Clicked outside! Increment counter to ${counter + 1}`);
setCounter(c => c + 1);
});
return (
<div>
<p>Click outside!</p>
<div id="container" ref={innerRef}>
Inside, counter: {counter}
</div>
</div>
);
};
ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>
Run code snippetHide results
Expand snippet
关键点
useOuterClick
利用可变引用来提供精益Client
API
- 在包含组件 (
[]
deps)的生命周期内_保持稳定的点击侦听器_
Client
可以设置回调而不需要通过以下方式记住它useCallback
- 回调主体可以访问最新的 props 和状态 -没有过时的闭包值
(iOS 的旁注)
iOS 通常只将某些元素视为可点击。要使外部点击起作用,请选择不同的点击侦听器document
- 没有向上,包括body
。例如,在 React 根上添加一个侦听器div
并扩展其高度,例如height: 100vh
,以捕获所有外部点击。资料来源:quirksmode.org