# react 自定义 hook
# 1. 什么是自定义 hook
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
函数名
:以 “use” 开头,比如 useState。为什么要以 “use” 开头。这是因为,如果不以 “use” 开头的话,无法判断这个函数是否包含对其内部 Hook 的调用,React 将无法自动检查这个自定义 Hook 是否违反了 Hook 的规则。
参数
:并没有特殊要求,只是 React 官网中有提到过,可以接收另一个 Hook 的返回值作为参数,实现在多个 Hook 之间传递信息的功能。
函数内部
:在函数内部可以调用其他的 Hook,但是要遵循两个规则:
只在最顶层使用 Hook ,不要在循环,条件或嵌套函数中调用 Hook,这是因为 React 是靠 Hook 调用的顺序来知道哪个 state 对于哪个 useState 的。
自定义 hooks 由 React 内置的 hook 或其他的自定义 hook 组成。因此一个自定义 hook 本质是一个或多个 hook 构成的新组合。如果一个自定义 hook 没有使用任何的内部 hook,那它就不是一个自定义 hook,也不应该以 "use" 为前缀。
# 2. 使用场景
让组件的通用逻辑复用更简单,可以把包含状态和事件处理的所有实现细节,移动到这个自定义 hook 中。另外,这个自定义 hook 返回一个数组,包含状态和一些更新状态函数。
# 3. 如何使用
自定义 hook 是定义在一个 js 或 ts 文件中,最后用 export 导出,故用 import 引入使用:
React 提供了多个内部 Hook,在自定义 Hook 的内部,一般都是利用内部 Hook,进行组装和扩展,来自定义各种功能的 Hook。
其中比较常见使用的 hooks 是useState
, useRef
, useEffect/useLayoutEffect
。
而从一个自定义 hook 中需要返回多个值时最佳的做法是返回一个数组,并利用数组进行解构。
# 4. 案例
实现一个检测 React 组件外部点击事件的 useOutsideClick
,其使用方式大致如下所示
function App() { | |
const handleClickOutside = () => { | |
//dosomething | |
}; | |
const ref = useOutsideClick(handleClickOutside); | |
const handleClick = () => { | |
//dosomething | |
}; | |
return ( | |
<div> | |
<button ref={ref} type="button" onClick={handleClick}> | |
Count: {count} | |
</button> | |
</div> | |
); | |
} |
从上述的使用方式上,可以首先写出如下的代码,输入 callback
,输出一个 ref
const useOutsideClick = (callback) => { | |
const ref = React.useRef(); | |
// 具体代码 | |
return ref; | |
}; |
由于这个 hook
主要是处理点击事件的,所以首先需要添加点击事件,并且在销毁时移除添加的事件,为了能监听所有的点击事件,是在 document 上添加。
const useOutsideClick = (callback) => { | |
const ref = React.useRef(); | |
React.useEffect(() => { | |
const handleClick = (event) => { | |
callback(); | |
}; | |
document.addEventListener('click', handleClick); | |
return () => { | |
document.removeEventListener('click', handleClick); | |
}; | |
}, []); | |
return ref; | |
}; |
目前版本所有点击都会触发 callback
,所以接下来就是过滤掉自身触发的 onclick
事件。
const useOutsideClick = (callback) => { | |
const ref = React.useRef(); | |
React.useEffect(() => { | |
const handleClick = (event) => { | |
if (ref.current && !ref.current.contains(event.target)) { | |
callback(); | |
} | |
}; | |
document.addEventListener('click', handleClick); | |
return () => { | |
document.removeEventListener('click', handleClick); | |
}; | |
}, [ref]); | |
return ref; | |
}; |
上述的代码已经满足了要求,但是还有一点可以改进的,就是可能某些场景下,存在一个触发事件的边界,即外部可以通过 stopPropagation
来停止事件的传播。如下所示:
function App() { | |
const handleClickOutside = () => { | |
//dosomething | |
}; | |
const ref = useOutsideClick(handleClickOutside); | |
const handleClick = () => { | |
//dosomething | |
}; | |
const handleHeaderClick = (event) => { | |
// do something | |
event.stopPropagation(); | |
}; | |
return ( | |
<div onClick={handleHeaderClick}> | |
<button ref={ref} type="button" onClick={handleClick}> | |
Count: {count} | |
</button> | |
</div> | |
); | |
} |
目前的版本并不适用与当前情况,因为事件中途停止冒泡了,所以无法触发在 document
上的 callback
为了实现上述功能,可以将冒泡阶段的事件处理放在捕获阶段进行处理。
const useOutsideClick = (callback) => { | |
const ref = React.useRef(); | |
React.useEffect(() => { | |
const handleClick = (event) => { | |
if (ref.current && !ref.current.contains(event.target)) { | |
callback(); | |
} | |
}; | |
document.addEventListener('click', handleClick, true); | |
return () => { | |
document.removeEventListener('click', handleClick, true); | |
}; | |
}, [ref]); | |
return ref; | |
}; |
# 5. 参考
- React Hook: Detect click outside of Component
- React Hook: Using the Local Storage
- React Hook: Check if Overflow
- React Hook: Get Scroll Direction
- React Hook: Get Scrollbar Width