一.什么是useHooks?
useHooks,是一款为 React 开发场景而生的 Hooks 库。
官网地址 GitHub地址
1.特性
全新的 useHooks 主要具备以下特性:
- 🚀 易学易用:直接通过 npm 下载即可使用
- 🔍 内置全文搜索:标题、正文、demo 等内容均可被搜索,支持多关键词搜索
- 🎨 包含丰富的基础 Hooks:如 useCreation、useLatest 等基础 hooks 为 react 开发赋能
- 🚥 可靠的代码健壮:使用 Typescript 构建,提供完善的类型定义文件
- 💎 完善的文档能力:支持文档记录,支持 demo 演示
2.问题反馈
如果在使用过程中发现任何问题、或者有改善建议,欢迎在 GitHub Issues 进行反馈:issues
3.下载
npm install @csshero/usehooks
# or
yarn add @csshero/usehooks
# or
pnpm add @csshero/usehooks
4.使用示例
只需从 @csshero/usehooks
导入你需要的函数
import { useUpdate } from '@csshero/usehooks';
import React from 'react';
export default () => {
const update = useUpdate();
return (
<>
<div>Time: {Date.now()}</div>
<button type="button" onClick={update} style={{ marginTop: 8 }}>
update
</button>
</>
);
};
二.源码分析
1. 高级部分
1.1 useCreation
1.1.1 介绍
useCreation
是 useMemo
的替代品。
-
这个hooks是用来替换useMemo,因为useMemo虽然也可以进行数据的缓存,但是它不能保证缓存的值一定不会被重新计算,我记得我在我的useHooks的官网里也提到了 在React官方文档里说 在未来React可能会增加丢弃缓存的特性,useMemo仅仅只是作为性能优化的手段,不然的话使用state或者ref。
-
意思就是如果你只是为了性能优化而使用 useMemo,那么这种可能的缓存丢弃并不会影响你的业务逻辑;你的代码在没有 useMemo 的情况下也能正常工作,只是性能会稍微差一些。
但如果你的业务逻辑需要依赖一个“持久稳定”的值——也就是说,这个值在整个组件生命周期内都必须保持不变(除非你主动更新它),那么 useMemo 可能就不合适,因为它可能会被 React 清理掉。这时,更合适的做法是使用 state 变量或者 ref,因为它们不会因为 React 的内部优化策略而意外丢失。
-
useCreation其实就是用Ref包裹了一个对象,这个对象的属性名有dependencies、init和最终结果的obj,每次会判断dependencies是否相同,要是不相同则重新运行factory函数,赋值给返回的obj,然后把新的dependencies赋值给旧的dependencies
1.1.2 源码分析
- isSameDep 负责判断传递的俩个数组是否相等
const isSameDep = (
oldDep: React.DependencyList,
newDep: React.DependencyList,
) => {
if (oldDep.length !== newDep.length) return false;
return oldDep.every((item, index) => Object.is(item, newDep[index]));
};
export default isSameDep;
- 用useRef保证稳定的引用,只要依赖的
deps
没有变化则不改变返回的factory()
init
用于判断是否进行初始化了,默认设置为 false- 只有当
init
为false
或者 依赖的deps
数组发生改变才会修改 返回的factory()
值,同时修改deps
为最新的deps
import { useRef } from 'react';
import isSameDep from '../utils/isSameDep';
const useCreation = <T>(factory: () => T, deps: React.DependencyList) => {
const { current } = useRef({
obj: null as T,
deps,
init: false,
});
if (!current.init || !isSameDep(current.deps, deps)) {
current.obj = factory();
current.deps = deps;
current.init = true;
}
return current.obj;
};
export default useCreation;
1.2 useLatest
1.2.1 介绍
useLatest
用于返回当前最新值的 Hook,可以避免闭包陷阱问题
闭包陷阱产生的原因就是 useEffect 等 hook 里用到了某个 state,但是没有加到 deps 数组里,这样导致 state 变了却没有执行新传入的函数,依然引用的之前的 state。
1.2.2 源码分析
-
源码非常简单,只需要将传入的 value 值用 useRef 包裹起来
-
为了保证
valRef.current
始终包含最新的value
,我们在每次渲染时都需要手动更新valRef.current = value
-
为什么返回valRef 不直接返回 valRef.current
- 如果在自定义 hook 内部直接返回
valRef.current
,那么每次调用这个 hook 时,你拿到的是那一刻的值快照。虽然在当前渲染中这个值是最新的,但如果你在其他地方(例如异步回调中)捕获了这个返回值,它就固定为那次渲染时的状态,不会随着后续的更新而改变。 - React 中
useRef
返回的对象在整个组件生命周期中是稳定不变的。返回整个 ref 对象,外部使用者可以在任何时候通过访问.current
来获取最新的值,而不必担心因捕获值快照而导致数据不更新的问题。
- 如果在自定义 hook 内部直接返回
import { useRef } from 'react';
const useLatest = <T>(value: T) => {
const valRef = useRef(value);
valRef.current = value;
return valRef;
};
export default useLatest;
1.3 useMemoizedFn
1.3.1 介绍
持久化 function 的 Hook,一般情况下,可以使用 useMemoizedFn 完全代替 useCallback, 特殊情况见FAQ
在某些场景中,我们需要使用 useCallback 来记住一个函数,但是在第二个参数 deps 变化时,会重新生成函数,导致函数地址变化。
const [state, setState] = useState('');
// 在 state 变化时,func 地址会变化
const func = useCallback(() => {
console.log(state);
}, [state]);
使用 useMemoizedFn,可以省略第二个参数 deps,同时保证函数地址永远不会变化。
const [state, setState] = useState('');
// func 地址永远不会变化
const func = useMemoizedFn(() => {
console.log(state);
});
FAQ
useMemoizedFn
返回的函数没有继承 fn 自身的属性?
useMemoizedFn
返回的函数与传入的 fn 的引用完全不同,且没有继承 fn 自身的属性。如果想要持久化后函数自身的属性不丢失,目前 useMemoizedFn
满足不了,请降级使用 useCallback
、useMemo
。
Related issues: 详情
// useMemoizedFn返回的函数是一个新的函数,只是在新函数中调用了传入的 fn
return function (...args) {
return fnRef.current.apply(this, args);
};
1.3.2 源码解析
- 对传递过来的 fn 进行一层 useRef 包裹,为了保证后面 memoizedFn.current 中每次调用时能获取到当前最新的fn
- 如果不把 fn 用 useRef 包裹,稳定的 memoizedFn 在首次创建时就会闭包捕获那个渲染周期中的 fn 版本,即便后续渲染中 fn 发生了变化,也无法反映最新的逻辑
- memoizedFn 则是用来
调用需要记忆的函数
,memoizedFn.current是一个固定的函数,在这个函数里面 调用了最新的 fn,并且把this 设置为 memoizedFn.current 的 this
import { useMemo, useRef } from 'react';
type FnType = (...args: any[]) => any;
const useMemoizedFn = <T extends FnType>(fn: T): T => {
const fnRef = useRef<T>(fn);
fnRef.current = useMemo<T>(() => fn, [fn]);
const memoizedFn = useRef<FnType>();
if (!memoizedFn.current) {
memoizedFn.current = function (...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
};
export default useMemoizedFn;
2.生命周期部分
2.1 useMount
2.1.1 介绍
只在组件初始化时执行的 Hook
2.1.2 源码解析
- 将传入的函数放入到 useEffect中,第二个参数为 []。这样只有当前组件被挂起时才会调用
import { useEffect } from 'react';
const useMount = (callback: () => void) => {
useEffect(() => {
callback();
}, []);
};
export default useMount;
2.2 useUnmount
2.2.1 介绍
在组件卸载(unmount)时执行的 Hook
2.2.2 源码解析
- 使用 useLatest 包裹一下 卸载时执行的fn,保证卸载时执行的fn是最新的
import { useEffect } from 'react';
import useLatest from '../useLatest';
const useUnmount = (callback: () => void) => {
const CallbackRef = useLatest(callback);
// 返回值中调用fn的最新值
useEffect(() => () => CallbackRef.current(), []);
};
export default useUnmount;
3. 副作用部分
3.1 useTimeout
3.1.1 介绍
一个可以处理 setTimeout 计时器函数的 Hook
3.1.2 源码解析
- 给传递进来的 callback 进行一层useRef嵌套,方便获取到最新的函数,如果不使用useRef嵌套,那放入到setTImeout里要执行的函数则无法执行最新的值,只能执行初始时函数的旧值
- 给setTimeout传递函数时需要在外侧包裹一层函数,不能直接
setTimeout(callbackRef.current, delay)
,因为直接传入callbackRef.current
会在调用 setTimeout 时立即获取当时的函数引用,并固定下来。用() => {}
包裹一层后setTimeout会在delay后执行() => {}
在这函数里又调用了callbackRef.current()
因此可以保证调用时,才会去访问最新的callbackRef.current
,直接setTimeout(callbackRef.current, delay)
则相当于在delay直接执行当时传入的函数,因为是先放到宏任务里面
import { useCallback, useEffect, useRef } from 'react';
type FnType = () => void;
const useTimeout = (callback: FnType, delay: number = 1000): FnType => {
const timer = useRef<ReturnType<typeof setTimeout>>();
const callbackRef = useRef<FnType>(callback);
const clearFn = useCallback(() => clearTimeout(timer.current), []);
// 更新 ref 中的 callback
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
timer.current = setTimeout(() => {
callbackRef.current();
}, delay);
return clearFn;
}, [delay]);
return clearFn;
};
export default useTimeout;
3.2 useInterval
3.2.1 介绍
一个可以处理 setInterval 的 Hook
3.2.2 源码解析
- 整体实现和 useTimeout 很像 不再过多赘述
import { useCallback, useEffect, useRef } from 'react';
type FnType = () => void;
const useInterval = (
callback: FnType,
delay: number = 1000,
immediate: boolean = false,
): FnType => {
const timerRef = useRef<ReturnType<typeof setInterval>>();
const callbackRef = useRef<FnType>(callback); // 用于保存最新的 callback
const clearFn = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
}, []);
// 每次渲染时更新 callbackRef 的值
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
if (immediate) {
callbackRef.current();
}
timerRef.current = setInterval(() => {
callbackRef.current(); // 每次都读取最新的 callback
}, delay);
return clearFn;
}, [delay]);
return clearFn;
};
export default useInterval;
3.3 useUpdate
3.3.1 介绍
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染
3.3.2 源码分析
- 基于useState实现,每次点击updata函数,都会重新设置 state 为新的空对象
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
const useUpdate = () => {
const [, setState] = useState({});
const updata = useMemoizedFn(() => setState({}));
return updata;
};
export default useUpdate;
3.4 useDebounceFn
3.4.1 介绍
用来处理防抖函数的 Hook
3.4.2 源码分析
- 依赖 外部的lodash库,然后根据需要传递对应的参数
import _ from 'lodash-es';
import useCreation from '../useCreation';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
interface Option {
wait?: number;
leading?: boolean;
trailing?: boolean;
maxWait?: number;
}
type FnType = (...args: any[]) => any;
const useDebounceFn = (callback: FnType, option: Option) => {
// 设置默认值
option.leading = option?.leading ?? false;
option.trailing = option?.trailing ?? true;
option.wait = option?.wait ?? 1000;
const callbackRef = useLatest(callback);
// 保存_.debounce()返回的函数
const debounce = useCreation(
() =>
_.debounce(
(...args) => {
callbackRef.current(...args);
},
option.wait,
option,
),
[],
);
useUnmount(() => {
debounce.cancel();
});
return {
run: debounce,
cancel: debounce.cancel,
flush: debounce.flush,
};
};
export default useDebounceFn;
3.5 useThrottleFn
3.5.1 介绍
用来处理节流函数的 Hook
3.5.2 源码分析
- 依赖 外部的lodash库,然后根据需要传递对应的参数
import _ from 'lodash-es';
import useCreation from '../useCreation';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
interface ThrottleOptions {
wait?: number;
leading?: boolean;
trailing?: boolean;
}
interface ThrottledFn<T extends FnType> {
run: (...args: Parameters<T>) => ReturnType<T>;
cancel: () => void;
flush: () => void;
}
type FnType = (...args: any[]) => any;
const useThrottleFn = <T extends FnType>(
callback: T,
option: ThrottleOptions,
): ThrottledFn<T> => {
// 设置默认值
option.leading = option?.leading ?? true;
option.trailing = option?.trailing ?? true;
option.wait = option?.wait ?? 1000;
const callbackRef = useLatest(callback);
// 保存_.debounce()返回的函数
const throttle = useCreation(
() =>
_.throttle(
(...args: Parameters<T>): ReturnType<T> => {
return callbackRef.current(...args);
},
option.wait,
option,
),
[],
);
useUnmount(() => {
throttle.cancel();
});
return {
run: throttle,
cancel: throttle.cancel,
flush: throttle.flush,
};
};
export default useThrottleFn;
4. 状态部分
4.1 useBoolean
4.1.1 介绍
优雅的管理 boolean 状态的 Hook
4.1.2 源码分析
- 定义 state 默认为 false
- 根据不同的方法进行切换
import { useState } from 'react';
interface Actions {
set: (value: boolean) => void;
setTrue: () => void;
setFalse: () => void;
toggle: () => void;
}
const useBoolean = (defaultValue: boolean = false): [boolean, Actions] => {
const [value, setValue] = useState(defaultValue);
const actions: Actions = {
set: setValue,
setTrue: () => setValue(true),
setFalse: () => setValue(false),
toggle: () => setValue((v) => !v),
};
return [value, actions];
};
export default useBoolean;
4.2 useToggle
4.2.1 介绍
用于在两个状态值间切换的 Hook
4.2.2 源码分析
-
基于函数的重载,来根据传递不同的参数类型,返回不同的结果
-
useBoolean的升级版,根据传递不同的参数,返回不同的结果
import { useMemo, useState } from 'react';
interface Actions<T> {
set: (value: T) => void;
setToDefault: () => void;
setToReverse: () => void;
toggle: () => void;
}
function useToggle(): [boolean, Actions<boolean>];
function useToggle<T>(defaultValue: T): [T, Actions<T>];
function useToggle<T, R>(defaultValue: T, reverseValue: R): [T, Actions<T | R>];
function useToggle<D, R>(defaultValue: D = false as D, reverseValue?: R) {
const [value, setValue] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
const reverseValueOrigin = (
reverseValue === undefined ? !defaultValue : reverseValue
) as D | R;
const toggle = () => {
setValue((v) => (v === defaultValue ? reverseValueOrigin : defaultValue));
};
const setToDefault = () => setValue(defaultValue);
const setToReverse = () => setValue(reverseValueOrigin);
const set = (v: D | R) => setValue(v);
return { toggle, setToDefault, setToReverse, set };
}, []);
return [value, actions];
}
export default useToggle;
4.3 useThrottleValue
4.3.1 介绍
用来对 值
处理节流的 Hook
4.3.2 源码分析
- 将外部传递进来的值作为useState的初始值
- 每次当外部传递的value变化时,都会触发useThrottleFn的run方法
import { useEffect, useState } from 'react';
import useThrottleFn from '../useThrottleFn';
interface ThrottleOptions {
wait?: number;
leading?: boolean;
trailing?: boolean;
}
const useThrottleValue = <T>(value: T, options: ThrottleOptions) => {
const [throttled, setThrottled] = useState(value);
const { run } = useThrottleFn(() => {
setThrottled(value);
}, options);
useEffect(() => {
run();
}, [value]);
return throttled;
};
export default useThrottleValue;
4.4 useDebounceValue
4.4.1 介绍
用来对 值
处理防抖的 Hook
4.4.2 源码分析
- 将外部传递进来的值作为useState的初始值
- 每次当外部传递的value变化时,都会触发useDebounceFn的run方法
import { useEffect, useState } from 'react';
import useDebounceFn from '../useDebounceFn';
interface Option {
wait?: number;
leading?: boolean;
trailing?: boolean;
maxWait?: number;
}
const useDebounceValue = <T>(value: T, options: Option) => {
const [debouncedValue, setDebouncedValue] = useState(value);
const { run } = useDebounceFn(() => {
setDebouncedValue(value);
}, options);
useEffect(() => {
run();
}, [value]);
return debouncedValue;
};
export default useDebounceValue;
4.5 useMap
4.5.1 介绍
管理 Map 类型状态的 Hook
4.5.2 使用的主要事项
在给 useMap 传递参数时 因为参数的类型是个可以迭代的集合,其中每个元素都是一个包含键和值的二元元组。但是在默认情况下,TypeScript 会把写的元组推断为数组类型
const test = ['a', 'b']; // test: string[]
因此需要给传递的参数进行声明
const data: [string, string][] = [['a', 'b']];
const [map, { set, setAll, remove, reset, get }] = useMap<string, string>(data);
const [map, { set, setAll, remove, reset, get }] = useMap<string, string>([
['msg', 'hello world'],
['info', 'hello react'],
] as const);
以上俩种都可以传递正确的参数类型
4.5.3 源码分析
- 接收的参数必须是一个可迭代对象,其每个元素都应当是一个长度为 2 的数组,表示一个键值对
- 每次操作都需要新建一个temp Map,因为React的数据不可变性
- 直接修改 prev 会导致对原有状态的副作用,可能会在其他依赖同一状态的地方引发难以察觉的 bug。通过复制一份 Map(即用 temp),再进行修改,可以保证每次更新返回的是一个全新的对象,这符合不可变更新的最佳实践。
import { useState } from 'react';
import useCreation from '../useCreation';
interface Actions<K, V> {
set: (key: K, value: V) => void;
get: (key: K) => V | undefined;
remove: (key: K) => void;
reset: () => void;
setAll: (newMap: Iterable<readonly [K, V]>) => void;
}
const useMap = <K, V>(
initValue?: Iterable<readonly [K, V]>,
): [Map<K, V>, Actions<K, V>] => {
const initMap = () => new Map(initValue);
const [map, setMap] = useState<Map<K, V>>(initMap);
const actions = useCreation(() => {
const set = (key: K, value: V) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, value);
return temp;
});
};
const get = (key: K) => map.get(key);
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(initMap());
const setAll = (newMap: Iterable<readonly [K, V]>) => {
setMap(new Map(newMap));
};
return {
set,
get,
remove,
reset,
setAll,
};
}, []);
return [map, actions];
};
export default useMap;
4.6 useSet
4.6.1 介绍
管理 Set 类型状态的 Hook
4.6.2 源码分析
- 和useMap很像,不再赘述了
import { useState } from 'react';
import useMemoizedFn from '../useMemoizedFn';
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => new Set(initialValue);
const [set, setSet] = useState<Set<K>>(getInitValue);
const add = (key: K) => {
if (set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.add(key);
return temp;
});
};
const remove = (key: K) => {
if (!set.has(key)) {
return;
}
setSet((prevSet) => {
const temp = new Set(prevSet);
temp.delete(key);
return temp;
});
};
const reset = () => setSet(getInitValue());
return [
set,
{
add: useMemoizedFn(add),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
},
] as const;
}
export default useSet;