React
提供了很多的 Hook
,比如 useState
, useCallback
等等...本文,我们来谈谈怎么自定义 Hook
。
命名约定
自定义的 Hook
的名称应该是以 use
开头,以便区分普通函数和 Hook
。
比如 usePlayAudio.ts
就应该是一个 Hook
。
使用现有的 Hook
我们在自定义 Hook
中,可以使用 React
提供的内置的 Hook
,比如 useState
, useEffect
等。
// useMount.ts
import { useEffect, useRef } from "react";
/**
* 组件挂载时候立即执行
* @param fn
*/
function useMount(fn?: () => void, clear?: () => void) {
const unmountRef = useRef<boolean>(false);
// 可以拿到 useState 的初始值
useEffect(() => {
unmountRef.current = false;
fn?.();
return () => {
clear?.();
unmountRef.current = true;
}
}, []);
return {
unmountRef,
}
}
export default useMount;
我们使用的时候,就像平常使用自带的 Hook
那样去使用它,比如👇
// usePlayAudio.ts
import useMount from "path/to/useMount";
export function usePlayAudio() {
useMount(() => {
// do something
})
}
上面,我们在自定义的 usePlayAudio
中使用自定义的 useMount
。
返回值
自定义的 Hook
返回值可以是任何我们需要的值。
比如返回函数👇
// useAutoScreen.ts
export const useAutoScreen = () => {
const timeOutRef = useRef<NodeJS.Timeout | null>(null);
// ...
const handleCancel = () => {
timeOutRef.current && clearTimeout(timeOutRef.current);
}
return {
handleCancel,
};
}
比如返回状态👇
// useStartPrint.ts
export function useStartPrint() {
const [printNowNumber, setPrintNowNumber] = useState(0);
const [printingImg, setPrintingImg] = useState<ReactNode>(null);
// ...
return {
printNowNumber,
printingImg
}
}
当然,我们也可以什么都返回,在自定义的 Hook
处理了相关的逻辑。
案例
我们上面讲解了自定义 Hook
需要注意的点。下面,我们来编写一个倒计时的自定义 Hook
。相关的代码如下:
// useLatestRef.ts
/**
* 随着组件生成或者更新拿取最新的状态/函数(包括状态)
* @param value
* @returns ref
*/
const useLatestRef = <T>(value: T): React.MutableRefObject<T> => {
const ref = useRef(value);
ref.current = value;
return ref;
};
export default useLatestRef;
自定义卸载的钩子函数👇
/**
* 卸载时以最新的函数执行
* @param fn
*/
function useUnMount(fn: () => void) {
const isClearRef = useRef(false);
const ref = useLatestRef(fn);
useEffect(
// 这个是简写的方式
() => () => {
isClearRef.current = true;
ref.current?.();
},
[]
);
return isClearRef;
}
export default useUnMount;
// useCountDown.ts
// 循环设置
export function loopSetTimeout({
isContinue,
awaitTimeStamp,
ref,
}: {
sContinue: () => boolean | Promise<boolean>;
awaitTimeStamp: number;
ref?: {
value: NodeJS.Timeout;
};
}) {
const timeOut = setTimeout(async () => {
const bool = await isContinue();
bool && loopSetTimeout({ isContinue, awaitTimeStamp, ref });
clearTimeout(timeOut);
}, awaitTimeStamp);
if (ref) {
ref.value = timeOut;
}
return timeOut;
}
export interface ICountDownParams {
seconds: number;
isStart?: boolean;
isStop?: boolean;
}
// 这里 isStart 和 isStop 是非必填项,都是默认值为 true
export const useCountDown = ({ seconds, isStart = true, isStop = false }: ICountDownParams) {
const [countDown, setCountDown] = useState(seconds);
const isStopRef = useRef(false);
const timeoutRef = useRef({ value: (0 as unknown) as NodeJS.Timeout });
// useUnMount 是自定义的卸载的钩子函数,参考上面的 useMount 钩子函数,代码如上
useUnMount(() => {
timeoutRef.current.value && clearTimeout(timeoutRef.current.value)
})
const loopCountDown = () => {
loopSetTimeout({
isContinue: async () => {
if (isStopRef.current) {
return false;
}
// 如果没有停止,我们就对之前的计数进行减去 1
setCountDown((prev) => {
prev--;
isStopRef.current = prev === 0;
return prev;
});
return !isStopRef.current;
},
awaitTimeStamp: 1000, // 毫秒
ref: timeoutRef.current,
})
}
const loopCountDownRef = useLatestRef(loopCountDown);
useEffect(() => {
if (isStart && isStopRef.current && !isStop) {
// 清理之前的定时器,保证不干扰
timeoutRef.current.value && clearTimeout(timeoutRef.current.value);
loopCountDownRef.current();
}
isStopRef.current = isStop;
}, [isStop, isStart, loopCountDownRef]);
useEffect(() => {
if (isStart && seconds) {
loopCountDownRef.current();
}
}, [isStart, seconds, loopCountDownRef]);
const updateCoutDown = useCallback((seconds: number) => {
setCountDown(seconds);
}, []);
// 我们返回了当前的倒计时的时间和更新倒计时的函数
return {
countDown,
updateCoutDown,
};
}
接下来,我们来使用这个钩子函数,如下:
// demo.tsx
// 引入自定义的钩子函数返回的状态或者函数
const { countDown } = useCountDown({ seconds: 30 });
return (
<>
<span>{ countDown }</span>
</>
);
一引入自定义的 useCountDown
这个钩子函数,则就开始进行倒计时的功能了。这个时候,你会看到页面上 countdown
变量数值的更改。
当然,上面自定义的钩子函数,我们还可以控制更新倒计时的时间,比如下面我们在另外一个自定义的钩子函数中使用:
// useShootCountDown.ts
// 一样的,我们首先得先引入
const { countDown, updateCoutDown } = useCountDown({
seconds: autoNextShootTime,
isStop: stopCoutDown,
isStart: startCountDown,
});
// 上面我们这次通过变量控制传参
useEffect(() => {
if (enableNextShootTime) {
updateCoutDown(autoNextShootTime);
// 先更新倒计时
setTimeout(() => {
setStopCoutDown(false);
}, 1000);
}
}, [enableNextShootTime, autoNextShootTime, updateCoutDown]);
嗯,上面就是本文说讲的话题。【完】❀