当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和Hook都是函数,所以也同样适用这种方式。
认识自定义Hook
自定义Hook是一个函数,其名称以use
开头,函数内部可以调用其他Hook。
在自定义Hook的顶层可以无条件地调用其他Hook(useState
, useEffect
)。
我们可以自由决定自定义Hook的参数和返回值。
自定义Hook必须以use开头,这样方便判断该函数内部是否调用了内部Hook,以及React能自动检查Hook是否违反了Hook的规则(见Hook规则部分)。
每次使用自定义Hook时,其中的state和副作用都是完全隔离的。
Hooks 和普通函数在语义上是有区别的,就在于函数中有没有用到其它 Hooks。
就是说如果你创建了一个 useXXX 的函数,但是内部并没有用任何其它 Hooks,那么这个函数就不是一个 Hook,而只是一个普通的函数。但是如果用了其它 Hooks ,那么它就是一个 Hook。
自定义Hook的特点
- 名字一定是以
use
开头的函数,这样 React 才能够知道这个函数是一个 Hook。 - 函数内部一定调用了其它的 Hooks,可以是内置的 Hooks,也可以是其它自定义 Hooks。这样才能够让组件刷新,或者去产生副作用。
可重用逻辑直接写一个工具类不就行了吗?为什么一定要通过Hook进行封装呢?
因为在 Hooks 中,你可以管理当前组件的
state
,从而将更多的逻辑写在可重用的 Hooks 中。但是要知道,在普通的工具类中是无法直接修改组件state
的,那么也就无法在数据改变的时候触发组件的重新渲染。
拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。
在这个场景下,我们不一定要把 Hooks 放到独立的文件中,而是可以和函数组件写在一个文件中。这么做的原因就在于,这些 Hooks 是和当前函数组件紧密相关的,所以写到一起,反而更容易阅读和理解。
例:
function MyComponent() {
const [id, setId] = useState(1);
const isOnline = useOnlineStatus(id);
return (
<>
// other nodes
</>
)
}
useState
为我们提供了id
的最新值,并把它做为参数传入useOnlineStatus
, 当id
改变时,useOnlineStatus
Hook会取消订阅前一个id
,并订阅新的id
。
例一:自定义 Hook 处理 LocalStorage 的存取
需求:希望把一些数据存储到
localStorage
中 - 不使用自定义Hook
不使用自定义Hook
import React, { useState, useEffect } from 'react'
export default function CustomDataStoreHook() {
const [name, setName] = useState(() => {
return JSON.parse(window.localStorage.getItem("name"))
});
useEffect(() => {
window.localStorage.setItem("name", JSON.stringify(name));
}, [name])
return (
<div>
<h2>CustomDataStoreHook: {name}</h2>
<button onClick={e => setName("gercke")}>设置name</button>
</div>
)
}
定义自定义Hook - useLocalStorage
import React,{useState, useEffect} from 'react';
function useLocalStorage(key) {
const [data, setData] = useState(() => {
return JSON.parse(window.localStorage.getItem(key))
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(data));
}, [data]);
return [data, setData];
}
export default useLocalStorage;
使用自定义Hook
import React, { useState, useEffect } from 'react';
import useLocalStorage from '../hooks/local-store-hook';
export default function CustomDataStoreHook() {
const [name, setName] = useLocalStorage("name");
return (
<div>
<h2>CustomDataStoreHook: {name}</h2>
<button onClick={e => setName("kobe")}>设置name</button>
</div>
)
}
例二:自定义Hook监听浏览器状态变化
需求:当
y > 300
时,显示Back to top
按钮
定义自定义Hook - useScroll
import { useState, useEffect } from 'react';
// 获取横向,纵向滚动条位置
const getPosition = () => {
return {
x: document.body.scrollLeft,
y: document.body.scrollTop,
};
};
const useScroll = () => {
// 定一个 position 这个 state 保存滚动条位置
const [position, setPosition] = useState(getPosition());
useEffect(() => {
const handler = () => {
setPosition(getPosition(document));
};
// 监听 scroll 事件,更新滚动条位置
document.addEventListener("scroll", handler);
return () => {
// 组件销毁时,取消事件监听
document.removeEventListener("scroll", handler);
};
}, []);
return position;
};
使用自定义Hook
import React, { useCallback } from 'react';
import useScroll from './useScroll';
function ScrollTop() {
const { y } = useScroll();
const goTop = useCallback(() => {
document.body.scrollTop = 0;
}, []);
const style = {
position: "fixed",
right: "10px",
bottom: "10px",
};
// 当滚动条位置纵向超过 300 时,显示返回顶部按钮
if (y > 300) {
return (
<button onClick={goTop} style={style}>
Back to Top
</button>
);
}
// 否则不 render 任何 UI
return null;
}
Hook 规则
- 只在最顶层使用Hook
- 不要在循环、条件或嵌套中调用Hook
- 只在React函数中调用Hook
- 在react的函数组件中调用Hook
- 在自定义Hook中调用其他Hook