Scene
useVirtualList
const useVirtualList = <T = any>(list: T[], options: Options<T>) => {
const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
// 每个item 的高度,可能是固定值或者是 func 动态
const itemHeightRef = useLatest(itemHeight);
// 外部容器的大小
const size = useSize(containerTarget);
// 是否是调用scrollTo 方法状态值
const scrollTriggerByScrollToFunc = useRef(false);
// 需要展示的列表
const [targetList, setTargetList] = useState<{ index: number; data: T }[]>([]);
// 获取可见列表的数量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
// 如果行高是固定值,直接取值
if (isNumber(itemHeightRef.current)) {
return Math.ceil(containerHeight / itemHeightRef.current);
}
// 否则逐项相加
let sum = 0;
let endIndex = 0;
for (let i = fromIndex; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
endIndex = i;
if (sum >= containerHeight) {
break;
}
}
// 计算数量
return endIndex - fromIndex;
};
// 根据滚动的高度获取当前 列表的offset,处理逻辑跟上面有点像
const getOffset = (scrollTop: number) => {
if (isNumber(itemHeightRef.current)) {
return Math.floor(scrollTop / itemHeightRef.current) + 1;
}
let sum = 0;
let offset = 0;
for (let i = 0; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
if (sum >= scrollTop) {
offset = i;
break;
}
}
return offset + 1;
};
// 获取上部高度,都是区分静态数值跟动态高度
const getDistanceTop = (index: number) => {
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
const height = list
.slice(0, index)
// @ts-ignore
.reduce((sum, _, i) => sum + itemHeightRef.current(i, list[index]), 0);
return height;
};
// 计算总高度
const totalHeight = useMemo(() => {
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
// @ts-ignore
return list.reduce((sum, _, index) => sum + itemHeightRef.current(index, list[index]), 0);
}, [list]);
// 计算列表数据,以及内容器的偏移值
const calculateRange = () => {
const container = getTargetElement(containerTarget);
const wrapper = getTargetElement(wrapperTarget);
if (container && wrapper) {
const { scrollTop, clientHeight } = container;
// 通过外容器的滚动制计算偏移
const offset = getOffset(scrollTop);
// 计算可视区域的数量
const visibleCount = getVisibleCount(clientHeight, offset);
// 计算数据开始部分跟结束部分,包括上部跟底部预加载部分
const start = Math.max(0, offset - overscan);
const end = Math.min(list.length, offset + visibleCount + overscan);
const offsetTop = getDistanceTop(start);
// @ts-ignore
wrapper.style.height = totalHeight - offsetTop + 'px';
// @ts-ignore
wrapper.style.marginTop = offsetTop + 'px';
setTargetList(
list.slice(start, end).map((ele, index) => ({
data: ele,
index: index + start,
})),
);
}
};
useEffect(() => {
if (!size?.width || !size?.height) {
return;
}
calculateRange();
}, [size?.width, size?.height, list]);
// 添加监听函数滚动时重新计算,但是如果是调用scrollTo,则忽略
useEventListener(
'scroll',
(e) => {
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
calculateRange();
},
{
target: containerTarget,
},
);
// 根据 索引 计算滚动高度,滚动到索引位置
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
return [targetList, useMemoizedFn(scrollTo)] as const;
};
useHistoryTravel
// 根据 step 返回 index
const dumpIndex = <T>(step: number, arr: T[]) => {
let index =
step > 0
? step - 1 // move forward
: arr.length + step; // move backward
if (index >= arr.length - 1) {
index = arr.length - 1;
}
if (index < 0) {
index = 0;
}
return index;
};
// 切割targetArr, 进行 dumpIndex 进行切割数组
const split = <T>(step: number, targetArr: T[]) => {
const index = dumpIndex(step, targetArr);
return {
_current: targetArr[index],
_before: targetArr.slice(0, index),
_after: targetArr.slice(index + 1),
};
};
export default function useHistoryTravel<T>(initialValue?: T) {
// 初始化值, present 为当前值, past 为过去值,future 为未来值
const [history, setHistory] = useState<IData<T | undefined>>({
present: initialValue,
past: [],
future: [],
});
const { present, past, future } = history;
const initialValueRef = useRef(initialValue);
// 重置值,以传入值优先为初始化默认值,
const reset = (...params: any[]) => {
const _initial = params.length > 0 ? params[0] : initialValueRef.current;
initialValueRef.current = _initial;
setHistory({
present: _initial,
future: [],
past: [],
});
};
// 新增值
const updateValue = (val: T) => {
setHistory({
present: val,
future: [],
past: [...past, present],
});
};
// 往前进
const _forward = (step: number = 1) => {
if (future.length === 0) {
return;
}
// 切割future数组,
const { _before, _current, _after } = split(step, future);
setHistory({
past: [...past, present, ..._before],
present: _current,
future: _after,
});
};
// 往后退
const _backward = (step: number = -1) => {
if (past.length === 0) {
return;
}
// 切割 last 数组
const { _before, _current, _after } = split(step, past);
setHistory({
past: _before,
present: _current,
future: [..._after, present, ...future],
});
};
// step 为正,则forward,否则 backward后退
const go = (step: number) => {
const stepNum = isNumber(step) ? step : Number(step);
if (stepNum === 0) {
return;
}
if (stepNum > 0) {
return _forward(stepNum);
}
_backward(stepNum);
};
return {
value: present,
backLength: past.length,
forwardLength: future.length,
setValue: useMemoizedFn(updateValue),
go: useMemoizedFn(go),
back: useMemoizedFn(() => {
go(-1);
}),
forward: useMemoizedFn(() => {
go(1);
}),
reset: useMemoizedFn(reset),
};
}
useNetwork
function useNetwork(): NetworkState {
// 初始化网络状态
const [state, setState] = useState(() => {
return {
since: undefined,
online: navigator?.onLine,
...getConnectionProperty(),
};
});
// 挂载时执行
useEffect(() => {
const onOnline = () => {
setState((prevState) => ({
...prevState,
online: true,
since: new Date(),
}));
};
const onOffline = () => {
setState((prevState) => ({
...prevState,
online: false,
since: new Date(),
}));
};
const onConnectionChange = () => {
setState((prevState) => ({
...prevState,
...getConnectionProperty(),
}));
};
// 在线离线监听
window.addEventListener(NetworkEventType.ONLINE, onOnline);
window.addEventListener(NetworkEventType.OFFLINE, onOffline);
// 网络连接状态监听
const connection = getConnection();
connection?.addEventListener(NetworkEventType.CHANGE, onConnectionChange);
return () => {
window.removeEventListener(NetworkEventType.ONLINE, onOnline);
window.removeEventListener(NetworkEventType.OFFLINE, onOffline);
connection?.removeEventListener(NetworkEventType.CHANGE, onConnectionChange);
};
}, []);
return state;
}
useSelections
export default function useSelections<T>(items: T[], defaultSelected: T[] = []) {
const [selected, setSelected] = useState<T[]>(defaultSelected);
const selectedSet = useMemo(() => new Set(selected), [selected]);
const isSelected = (item: T) => selectedSet.has(item);
// 选中 item,添加进 set
const select = (item: T) => {
selectedSet.add(item);
return setSelected(Array.from(selectedSet));
};
// 从 set 中删除
const unSelect = (item: T) => {
selectedSet.delete(item);
return setSelected(Array.from(selectedSet));
};
// 切换选中状态
const toggle = (item: T) => {
if (isSelected(item)) {
unSelect(item);
} else {
select(item);
}
};
const selectAll = () => {
items.forEach((o) => {
selectedSet.add(o);
});
setSelected(Array.from(selectedSet));
};
const unSelectAll = () => {
items.forEach((o) => {
selectedSet.delete(o);
});
setSelected(Array.from(selectedSet));
};
// 全不选
const noneSelected = useMemo(() => items.every((o) => !selectedSet.has(o)), [items, selectedSet]);
// 全不选
const allSelected = useMemo(
() => items.every((o) => selectedSet.has(o)) && !noneSelected,
[items, selectedSet, noneSelected],
);
// 既不是没有都没选,也不是全选
const partiallySelected = useMemo(
() => !noneSelected && !allSelected,
[noneSelected, allSelected],
);
const toggleAll = () => (allSelected ? unSelectAll() : selectAll());
return {
selected,
noneSelected,
allSelected,
partiallySelected,
setSelected,
isSelected,
select: useMemoizedFn(select),
unSelect: useMemoizedFn(unSelect),
toggle: useMemoizedFn(toggle),
selectAll: useMemoizedFn(selectAll),
unSelectAll: useMemoizedFn(unSelectAll),
toggleAll: useMemoizedFn(toggleAll),
} as const;
}
useTextSelection
const initState: State = {
text: '',
...initRect,
};
// 获取选中元素位置信息
function getRectFromSelection(selection: Selection | null): Rect {
if (!selection) {
return initRect;
}
if (selection.rangeCount < 1) {
return initRect;
}
const range = selection.getRangeAt(0);
const { height, width, top, left, right, bottom } = range.getBoundingClientRect();
return {
height,
width,
top,
left,
right,
bottom,
};
}
function useTextSelection(target?: BasicTarget<Document | Element>): State {
const [state, setState] = useState(initState);
const stateRef = useRef(state);
stateRef.current = state;
useEffectWithTarget(
() => {
const el = getTargetElement(target, document);
if (!el) {
return;
}
// 鼠标抬起时,获取选择文字
const mouseupHandler = () => {
let selObj: Selection | null = null;
let text = '';
let rect = initRect;
if (!window.getSelection) return;
selObj = window.getSelection();
text = selObj ? selObj.toString() : '';
if (text) {
rect = getRectFromSelection(selObj);
setState({ ...state, text, ...rect });
}
};
// 任意点击都需要清空之前的 range
const mousedownHandler = () => {
if (!window.getSelection) return;
if (stateRef.current.text) {
setState({ ...initState });
}
const selObj = window.getSelection();
if (!selObj) return;
selObj.removeAllRanges();
};
// 在当前元素上鼠标抬起事件
el.addEventListener('mouseup', mouseupHandler);
// 而在document设置鼠点击事件,是为了清除选中
document.addEventListener('mousedown', mousedownHandler);
return () => {
el.removeEventListener('mouseup', mouseupHandler);
document.removeEventListener('mousedown', mousedownHandler);
};
},
[],
target,
);
return state;
}
useCountDown
// 计算剩余的时间
const calcLeft = (t?: TDate) => {
if (!t) {
return 0;
}
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
const left = dayjs(t).valueOf() - new Date().getTime();
if (left < 0) {
return 0;
}
return left;
};
// 格式化时间
const parseMs = (milliseconds: number): FormattedRes => {
return {
days: Math.floor(milliseconds / 86400000),
hours: Math.floor(milliseconds / 3600000) % 24,
minutes: Math.floor(milliseconds / 60000) % 60,
seconds: Math.floor(milliseconds / 1000) % 60,
milliseconds: Math.floor(milliseconds) % 1000,
};
};
const useCountdown = (options?: Options) => {
const { targetDate, interval = 1000, onEnd } = options || {};
const [timeLeft, setTimeLeft] = useState(() => calcLeft(targetDate));
const onEndRef = useLatest(onEnd);
useEffect(() => {
if (!targetDate) {
// for stop
setTimeLeft(0);
return;
}
// 立即执行一次
setTimeLeft(calcLeft(targetDate));
const timer = setInterval(() => {
const targetLeft = calcLeft(targetDate);
setTimeLeft(targetLeft);
// 倒计时为0 ,清空计时器, 调用end 回调
if (targetLeft === 0) {
clearInterval(timer);
onEndRef.current?.();
}
}, interval);
return () => clearInterval(timer);
}, [targetDate, interval]);
// 重新获取格式化时间
const formattedRes = useMemo(() => {
return parseMs(timeLeft);
}, [timeLeft]);
return [timeLeft, formattedRes] as const;
};
useCounter
// 最大最小值校验
function getTargetValue(val: number, options: Options = {}) {
const { min, max } = options;
let target = val;
if (isNumber(max)) {
target = Math.min(max, target);
}
if (isNumber(min)) {
target = Math.max(min, target);
}
return target;
}
function useCounter(initialValue: number = 0, options: Options = {}) {
const { min, max } = options;
const [current, setCurrent] = useState(() => {
// 初始化校验值
return getTargetValue(initialValue, {
min,
max,
});
});
const setValue = (value: ValueParam) => {
setCurrent((c) => {
// 判断设置值
const target = isNumber(value) ? value : value(c);
return getTargetValue(target, {
max,
min,
});
});
};
// delta 进行增加
const inc = (delta: number = 1) => {
setValue((c) => c + delta);
};
const dec = (delta: number = 1) => {
setValue((c) => c - delta);
};
const set = (value: ValueParam) => {
setValue(value);
};
const reset = () => {
setValue(initialValue);
};
return [
current,
{
inc: useMemoizedFn(inc),
dec: useMemoizedFn(dec),
set: useMemoizedFn(set),
reset: useMemoizedFn(reset),
},
] as const;
}
LifeCycle
useMount
const useMount = (fn: () => void) => {
// 开发环境校验 fn 类型
if (process.env.NODE_ENV === 'development') {
if (!isFunction(fn)) {
console.error(
`useMount: parameter `fn` expected to be a function, but got "${typeof fn}".`,
);
}
}
// 利用空的依赖数组
useEffect(() => {
fn?.();
}, []);
};
useUnmount
const useUnmount = (fn: () => void) => {
if (process.env.NODE_ENV === 'development') {
if (!isFunction(fn)) {
console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
}
}
// 获取最新的 fn
const fnRef = useLatest(fn);
// 获取 ref 中的卸载函数, 返回给 react 调用
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
useUnmountedRef
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
// 利用 ref 和 useEffect 组合保存变量来获取组件是否卸载的状态
useEffect(() => {
unmountedRef.current = false;
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
State
useSetState
const useSetState = <S extends Record<string, any>>(
initialState: S | (() => S),
): [S, SetState<S>] => {
const [state, setState] = useState<S>(initialState);
// 空数组依赖
const setMergeState = useCallback((patch) => {
setState((prevState) => {
// 添加 fn 支持去更新获取新状态
const newState = isFunction(patch) ? patch(prevState) : patch;
return newState ? { ...prevState, ...newState } : prevState;
});
}, []);
return [state, setMergeState];
};
useBoolean
export default function useBoolean(defaultValue = false): [boolean, Actions] {
// 利用 toggle 函数去做 useBoolean 底层支持
const [state, { toggle, set }] = useToggle(defaultValue);
const actions: Actions = useMemo(() => {
const setTrue = () => set(true);
const setFalse = () => set(false);
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
};
}, []);
return [state, actions];
}
useToggle
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
// 使用 state 保存 默认值
const [state, setState] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
// 不传 reverseValue 则默认是 defaultValue的reverse
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
// 设置 toogle 值
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
const set = (value: D | R) => setState(value);
// 设置默认值
const setLeft = () => setState(defaultValue);
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
// useToggle ignore value change
// }, [defaultValue, reverseValue]);
}, []);
return [state, actions];
}
useUrlState
const useUrlState = <S extends UrlState = UrlState>(
initialState?: S | (() => S),
options?: Options,
) => {
type State = Partial<{ [key in keyof S]: any }>;
const { navigateMode = 'push', parseOptions, stringifyOptions } = options || {};
// parse 选项
const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
// 序列化选项
const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };
// 获取地址
const location = rc.useLocation();
// react-router v5
const history = rc.useHistory?.();
// react-router v6
const navigate = rc.useNavigate?.();
// 强制更新
const update = useUpdate();
const initialStateRef = useRef(
typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
);
// 解析 search
const queryFromUrl = useMemo(() => {
return parse(location.search, mergedParseOptions);
}, [location.search]);
// 合并, 这也是 history 能更新返回值的原因
const targetQuery: State = useMemo(
() => ({
...initialStateRef.current,
...queryFromUrl,
}),
[queryFromUrl],
);
const setState = (s: React.SetStateAction<State>) => {
// 支持 fn 生产
const newQuery = typeof s === 'function' ? s(targetQuery) : s;
// 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
// 2. update 和 history 的更新会合并,不会造成多次更新
update();
if (history) {
history[navigateMode]({
hash: location.hash,
search: stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
});
}
if (navigate) {
navigate(
{
hash: location.hash,
search: stringify({ ...queryFromUrl, ...newQuery }, mergedStringifyOptions) || '?',
},
{
replace: navigateMode === 'replace',
},
);
}
};
return [targetQuery, useMemoizedFn(setState)] as const;
};
useCookieState
function useCookieState(cookieKey: string, options: Options = {}) {
const [state, setState] = useState<State>(() => {
// 设置 cookie 的初始值
const cookieValue = Cookies.get(cookieKey);
if (isString(cookieValue)) return cookieValue;
// 支持方法默认值
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
});
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
setState((prevState) => {
// 获取新的 state
const value = isFunction(newValue) ? newValue(prevState) : newValue;
// undefined 移除
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
return value;
});
},
);
return [state, updateState] as const;
}
useLocalStorageState
// 是否是浏览器环境,同时兼容不同storage,如 lcoalstorage 或者 sessionStorage
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
export default useLocalStorageState;
export function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options?: Options<T>) {
let storage: Storage | undefined;
// https://github.com/alibaba/hooks/issues/800
try {
storage = getStorage();
} catch (err) {
console.error(err);
}
// 序列化
const serializer = (value: T) => {
if (options?.serializer) {
return options?.serializer(value);
}
return JSON.stringify(value);
};
// 反序列化
const deserializer = (value: string) => {
if (options?.deserializer) {
return options?.deserializer(value);
}
return JSON.parse(value);
};
// 获取存储的值,需要反序列化,拿不到就返回默认值
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
console.error(e);
}
if (isFunction(options?.defaultValue)) {
return options?.defaultValue();
}
return options?.defaultValue;
}
const [state, setState] = useState<T | undefined>(() => getStoredValue())
// key 变化时,重新设置 state 的值
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
const updateState = (value?: T | IFuncUpdater<T>) => {
// value为空,移除
if (isUndef(value)) {
setState(undefined);
storage?.removeItem(key);
} else if (isFunction(value)) {
// 如果为function,序列化后存入
const currentState = value(state);
try {
setState(currentState);
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
} else {
try {
// 直接序列化后存入
setState(value);
storage?.setItem(key, serializer(value));
} catch (e) {
console.error(e);
}
}
};
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
useSessionStorageState
跟 上面一样,只是 getStorage不同
useDebounce
function useDebounce<T>(value: T, options?: DebounceOptions) {
const [debounced, setDebounced] = useState(value);
const { run } = useDebounceFn(() => {
// 设置防抖的值
setDebounced(value);
}, options);
useEffect(() => {
// 每次依赖一变化,就触发 run
run();
}, [value]);
return debounced;
}
useThrottle
function useThrottle<T>(value: T, options?: ThrottleOptions) {
const [throttled, setThrottled] = useState(value);
const { run } = useThrottleFn(() => {
// 最后节流的state
setThrottled(value);
}, options);
// 依赖变化执行节流函数 run
useEffect(() => {
run();
}, [value]);
return throttled;
}
useMap
// 对map 增删改查
function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
// 初始换的缓存,利用闭包缓存
const getInitValue = () => {
return initialValue === undefined ? new Map() : new Map(initialValue);
};
const [map, setMap] = useState<Map<K, T>>(() => getInitValue());
const set = (key: K, entry: T) => {
setMap((prev) => {
const temp = new Map(prev);
temp.set(key, entry);
return temp;
});
};
const setAll = (newMap: Iterable<readonly [K, T]>) => {
setMap(new Map(newMap));
};
const remove = (key: K) => {
setMap((prev) => {
const temp = new Map(prev);
temp.delete(key);
return temp;
});
};
const reset = () => setMap(getInitValue());
const get = (key: K) => map.get(key);
return [
map,
{
// 防止闭包陷阱
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
] as const;
}
useSet
跟上面 map差不多
function useSet<K>(initialValue?: Iterable<K>) {
const getInitValue = () => {
return initialValue === undefined ? new Set<K>() : 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;
}
usePrevious
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);
function usePrevious<T>(
state: T,
shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
// 保存上次的值跟当前的值
const prevRef = useRef<T>();
const curRef = useRef<T>();
// 如果当前的值更上次的值不一样,更新,讲当次的值赋值给current,讲上次的值赋值给prev
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
useRafState
function useRafState<S>(initialState?: S | (() => S)) {
const ref = useRef(0);
const [state, setState] = useState(initialState);
// 值的更新在raf 中
const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
cancelAnimationFrame(ref.current);
ref.current = requestAnimationFrame(() => {
// 设置 state 的值,同时保存 raf 的id,方便在卸载时执行取消操作
setState(value);
});
}, []);
useUnmount(() => {
// 取消raf
cancelAnimationFrame(ref.current);
});
return [state, setRafState] as const;
}
useSafeState
function useGetState<S>(initialState?: S) {
const [state, setState] = useState(initialState);
const stateRef = useRef(state);
// 利用 ref 保存最新的state,在任何地方调用state都不会出现闭包陷阱
stateRef.current = state;
const getState = useCallback(() => stateRef.current, []);
return [state, setState, getState];
}
function useSafeState<S>(initialState?: S | (() => S)) {
// 获取卸载状态
const unmountedRef = useUnmountedRef();
const [state, setState] = useState(initialState);
const setCurrentState = useCallback((currentState) => {
// 组件卸载时,不进行状态更新
/** if component is unmounted, stop update */
if (unmountedRef.current) return;
setState(currentState);
}, []);
return [state, setCurrentState] as const;
}
Effect
useUpdateEffect
export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
(hook) => (effect, deps) => {
// 利用 ref 保存值,但是 react-refresh 时不会重置
const isMounted = useRef(false);
// for react-refresh
// react-refresh 会导致 effect 卸载,需要重置值
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
// ref 竞态,保证第一次不被执行
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
import { createUpdateEffect } from '../createUpdateEffect';
export default createUpdateEffect(useEffect)
useUpdateLayoutEffect
同上 useUpdateEffect
useAsyncEffect
function useAsyncEffect(
effect: () => AsyncGenerator<void, void, void> | Promise<void>,
deps?: DependencyList,
) {
// 判断是否是 AsyncGenerator
function isAsyncGenerator(
val: AsyncGenerator<void, void, void> | Promise<void>,
): val is AsyncGenerator<void, void, void> {
return isFunction(val[Symbol.asyncIterator]);
}
useEffect(() => {
const e = effect();
let cancelled = false;
async function execute() {
if (isAsyncGenerator(e)) {
while (true) {
const result = await e.next();
// 异步执行完毕,或者 effect 在检查点被卸载了
if (result.done || cancelled) {
break;
}
}
} else {
await e;
}
}
execute();
return () => {
// 如果在检查点 effect 清理了,跳出循环
cancelled = true;
};
}, deps);
}
useDebounceEffect
function useDebounceEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: DebounceOptions,
) {
const [flag, setFlag] = useState({});
const { run } = useDebounceFn(() => {
// 执行 flag 更新 ,处罚 effect 执行
setFlag({});
}, options);
// 依赖一改变跑一次防抖函数,防抖函数的卸载在useDebounceFn里
useEffect(() => {
return run();
}, deps);
// 只在更新阶段执行 effect,也就是 flag 不更新,这个effect不会执行
useUpdateEffect(effect, [flag]);
}
useDebounceFn
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
if (process.env.NODE_ENV === 'development') {
if (!isFunction(fn)) {
console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);
}
}
// fn 获取最新的函数
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
// 防抖清除
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
useThrottleFn
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
if (process.env.NODE_ENV === 'development') {
if (!isFunction(fn)) {
console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
// memo节流函数,同时利用 ref 防止闭包
const throttled = useMemo(
() =>
throttle(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
// 卸载时去掉节流
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled,
cancel: throttled.cancel,
flush: throttled.flush,
};
}
useThrottleEffect
function useThrottleEffect(
effect: EffectCallback,
deps?: DependencyList,
options?: ThrottleOptions,
) {
const [flag, setFlag] = useState({});
// 生成 节流函数
const { run } = useThrottleFn(() => {
setFlag({});
}, options);
// 依赖变化执行节流函数,但是只有触发 delay 内执行一次,利用 flag 执行 effect
useEffect(() => {
return run();
}, deps);
// 保证 mount 的时候不会执行 effect
useUpdateEffect(effect, [flag]);
}
export default useThrottleEffect;
useDeepCompareEffect
import { createDeepCompareEffect } from '../createDeepCompareEffect';
// 深度对比依赖度 effect
export default createDeepCompareEffect(useEffect);
useInterval
function useInterval(
fn: () => void,
delay: number | undefined,
options?: {
immediate?: boolean;
},
) {
const immediate = options?.immediate;
// ref 防止回调陷阱
const fnRef = useLatest(fn);
useEffect(() => {
if (!isNumber(delay) || delay < 0 || isNaN(delay)) {
console.warn(`delay should be a valid number but get ${delay}`);
return;
}
if (immediate) {
fnRef.current();
}
// 使用原生interval 执行回调
const timer = setInterval(() => {
fnRef.current();
}, delay);
return () => {
clearInterval(timer);
};
}, [delay]);
}
useRafInterval
interface Handle {
id: number | NodeJS.Timer;
}
// raf 模拟 interval
const setRafInterval = function (callback: () => void, delay: number = 0): Handle {
if (typeof requestAnimationFrame === typeof undefined) {
return {
id: setInterval(callback, delay),
};
}
let start = new Date().getTime();
const handle: Handle = {
id: 0,
};
const loop = () => {
const current = new Date().getTime();
// 如果 interval 满足,触发回调,同时重置 start
if (current - start >= delay) {
callback();
start = new Date().getTime();
}
handle.id = requestAnimationFrame(loop);
};
handle.id = requestAnimationFrame(loop);
return handle;
};
function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
return typeof cancelAnimationFrame === typeof undefined;
}
const clearRafInterval = function (handle: Handle) {
if (cancelAnimationFrameIsNotDefined(handle.id)) {
return clearInterval(handle.id);
}
cancelAnimationFrame(handle.id);
};
function useRafInterval(
fn: () => void,
delay: number | undefined,
options?: {
immediate?: boolean;
},
) {
// 是否需要立刻执行回调函数
const immediate = options?.immediate;
const fnRef = useLatest(fn);
useEffect(() => {
if (!isNumber(delay) || delay < 0) return;
if (immediate) {
fnRef.current();
}
// 开始 interval 回调
const timer = setRafInterval(() => {
fnRef.current();
}, delay);
return () => {
clearRafInterval(timer);
};
}, [delay]);
}
export default useRafInterval;
useTimeout
function useTimeout(fn: () => void, delay: number | undefined): void {
// 保存 fn 回调函数
const fnRef = useLatest(fn);
useEffect(() => {
// 判断是否正确的delay
if (!isNumber(delay) || delay < 0 || isNaN(delay)) {
console.warn(`delay should be a valid number but get ${delay}`);
return;
}
// 设置定时器
const timer = setTimeout(() => {
fnRef.current();
}, delay);
return () => {
clearTimeout(timer);
};
}, [delay]);
}
export default useTimeout;
useRafTimeout
使用 raf 去模拟 setTimeout
interface Handle {
id: number | NodeJS.Timeout;
}
const setRafTimeout = function (callback: () => void, delay: number = 0): Handle {
// 如果不支持 raf ,降级使用 setTimeout
if (typeof requestAnimationFrame === typeof undefined) {
return {
id: setTimeout(callback, delay),
};
}
const handle: Handle = {
id: 0,
};
const startTime = new Date().getTime();
const loop = () => {
const current = new Date().getTime();
// 判断事件是否到期了
if (current - startTime >= delay) {
callback();
} else {
// 继续使用递归使用 raf
handle.id = requestAnimationFrame(loop);
}
};
handle.id = requestAnimationFrame(loop);
return handle;
};
// 判断 cancelraf 是否支持
function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
return typeof cancelAnimationFrame === typeof undefined;
}
// 清空定时器
const clearRafTimeout = function (handle: Handle) {
if (cancelAnimationFrameIsNotDefined(handle.id)) {
return clearTimeout(handle.id);
}
cancelAnimationFrame(handle.id);
};
function useRafTimeout(fn: () => void, delay: number | undefined) {
const fnRef = useLatest(fn);
// delay 为依赖, 设置 raf 定时器
useEffect(() => {
if (!isNumber(delay) || delay < 0) return;
const timer = setRafTimeout(() => {
fnRef.current();
}, delay);
return () => {
clearRafTimeout(timer);
};
}, [delay]);
}
export default useRafTimeout;
useLockFn
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
// 利用 ref 保存竞态值来保证 fn 不会被重复执行
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
if (lockRef.current) return;
lockRef.current = true;
try {
const ret = await fn(...args);
lockRef.current = false;
return ret;
} catch (e) {
lockRef.current = false;
throw e;
}
},
[fn],
);
}
useUpdate
const useUpdate = () => {
const [, setState] = useState({});
// 强制赋值进行强制刷新
return useCallback(() => setState({}), []);
};
Dom
useEventListener
function useEventListener(eventName: string, handler: noop, options: Options = {}) {
const handlerRef = useLatest(handler);
useEffectWithTarget(
() => {
const targetElement = getTargetElement(options.target, window);
if (!targetElement?.addEventListener) {
return;
}
const eventListener = (event: Event) => {
return handlerRef.current(event);
};
// 元素监听
targetElement.addEventListener(eventName, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
});
return () => {
// 卸载监听函数
targetElement.removeEventListener(eventName, eventListener, {
capture: options.capture,
});
};
},
[eventName, options.capture, options.once, options.passive],
options.target,
);
}
useClickAway
export default function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void,
target: BasicTarget | BasicTarget[],
eventName: string | string[] = 'click',
) {
const onClickAwayRef = useLatest(onClickAway);
useEffectWithTarget(
() => {
const handler = (event: any) => {
const targets = Array.isArray(target) ? target : [target];
// 如果触发的事件的元素在 targets 内或者包含,则不触发事件
if (
targets.some((item) => {
const targetElement = getTargetElement(item);
return !targetElement || targetElement.contains(event.target);
})
) {
return;
}
onClickAwayRef.current(event);
};
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
eventNames.forEach((event) => document.addEventListener(event, handler));
return () => {
eventNames.forEach((event) => document.removeEventListener(event, handler));
};
},
Array.isArray(eventName) ? eventName : [eventName],
target,
);
}
useDocumentVisibility
type VisibilityState = 'hidden' | 'visible' | 'prerender' | undefined;
const getVisibility = () => {
// 兼容 ssr
if (!isBrowser) {
return 'visible';
}
return document.visibilityState;
};
function useDocumentVisibility(): VisibilityState {
const [documentVisibility, setDocumentVisibility] = useState(() => getVisibility());
// 监听 document 的 visibilitychange
useEventListener(
'visibilitychange',
() => {
setDocumentVisibility(getVisibility());
},
{
target: () => document,
},
);
return documentVisibility;
}
useDrop & useDrag
const useDrag = <T>(data: T, target: BasicTarget, options: Options = {}) => {
const optionsRef = useLatest(options);
useEffectWithTarget(
() => {
// 获取被拖拽的元素
const targetElement = getTargetElement(target);
if (!targetElement?.addEventListener) {
return;
}
const onDragStart = (event: React.DragEvent) => {
// 触发拖拽开始回调
optionsRef.current.onDragStart?.(event);
// 设置拖拽的内容, key 为 custom
event.dataTransfer.setData('custom', JSON.stringify(data));
};
const onDragEnd = (event: React.DragEvent) => {
optionsRef.current.onDragEnd?.(event);
};
// 定义可拖动目标。将我们希望拖动的元素的draggable属性设为true, from mdn
targetElement.setAttribute('draggable', 'true');
// 设置 监听函数
targetElement.addEventListener('dragstart', onDragStart as any);
targetElement.addEventListener('dragend', onDragEnd as any);
return () => {
targetElement.removeEventListener('dragstart', onDragStart as any);
targetElement.removeEventListener('dragend', onDragEnd as any);
};
},
[],
target,
);
};
const useDrop = (target: BasicTarget, options: Options = {}) => {
const optionsRef = useLatest(options);
// https://stackoverflow.com/a/26459269
const dragEnterTarget = useRef<any>();
useEffectWithTarget(
() => {
const targetElement = getTargetElement(target);
if (!targetElement?.addEventListener) {
return;
}
const onData = (
dataTransfer: DataTransfer,
event: React.DragEvent | React.ClipboardEvent,
) => {
// 获取拖拽的数据
const uri = dataTransfer.getData('text/uri-list');
const dom = dataTransfer.getData('custom');
if (dom && optionsRef.current.onDom) {
let data = dom;
try {
data = JSON.parse(dom);
} catch (e) {
data = dom;
}
optionsRef.current.onDom(data, event as React.DragEvent);
return;
}
if (uri && optionsRef.current.onUri) {
optionsRef.current.onUri(uri, event as React.DragEvent);
return;
}
// 拖拽的文件
if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) {
optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent);
return;
}
// 拖拽的文字
if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) {
dataTransfer.items[0].getAsString((text) => {
optionsRef.current.onText!(text, event as React.ClipboardEvent);
});
}
};
const onDragEnter = (event: React.DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragEnterTarget.current = event.target;
optionsRef.current.onDragEnter?.(event);
};
const onDragOver = (event: React.DragEvent) => {
event.preventDefault();
optionsRef.current.onDragOver?.(event);
};
const onDragLeave = (event: React.DragEvent) => {
if (event.target === dragEnterTarget.current) {
optionsRef.current.onDragLeave?.(event);
}
};
// 拖拽时
const onDrop = (event: React.DragEvent) => {
event.preventDefault();
onData(event.dataTransfer, event);
optionsRef.current.onDrop?.(event);
};
// 粘贴时
const onPaste = (event: React.ClipboardEvent) => {
onData(event.clipboardData, event);
optionsRef.current.onPaste?.(event);
};
// 设置监听函数
targetElement.addEventListener('dragenter', onDragEnter as any);
targetElement.addEventListener('dragover', onDragOver as any);
targetElement.addEventListener('dragleave', onDragLeave as any);
targetElement.addEventListener('drop', onDrop as any);
targetElement.addEventListener('paste', onPaste as any);
return () => {
targetElement.removeEventListener('dragenter', onDragEnter as any);
targetElement.removeEventListener('dragover', onDragOver as any);
targetElement.removeEventListener('dragleave', onDragLeave as any);
targetElement.removeEventListener('drop', onDrop as any);
targetElement.removeEventListener('paste', onPaste as any);
};
},
[],
target,
);
};
useEventTarget
function useEventTarget<T, U = T>(options?: Options<T, U>) {
const { initialValue, transformer } = options || {};
const [value, setValue] = useState(initialValue);
// 保存转化回调
const transformerRef = useLatest(transformer);
// 保存初始换的设置函数
const reset = useCallback(() => setValue(initialValue), []);
const onChange = useCallback((e: EventTarget<U>) => {
const _value = e.target.value;
if (isFunction(transformerRef.current)) {
return setValue(transformerRef.current(_value));
}
// no transformer => U and T should be the same
return setValue(_value as unknown as T);
}, []);
return [
value,
{
onChange,
reset,
},
] as const;
}
useExternal
// 保存 尾部
const EXTERNAL_USED_COUNT: Record<string, number> = {};
export type Status = 'unset' | 'loading' | 'ready' | 'error';
interface loadResult {
ref: Element;
status: Status;
}
// 加载js
const loadScript = (path: string, props = {}): loadResult => {
// 确保js是否已经被加载过
const script = document.querySelector(`script[src="${path}"]`);
if (!script) {
const newScript = document.createElement('script');
newScript.src = path;
Object.keys(props).forEach((key) => {
newScript[key] = props[key];
});
// 设置状态
newScript.setAttribute('data-status', 'loading');
// 加载
document.body.appendChild(newScript);
return {
ref: newScript,
status: 'loading',
};
}
// 返回 加载状态和加载的元素
return {
ref: script,
status: (script.getAttribute('data-status') as Status) || 'ready',
};
};
// 加载 css
const loadCss = (path: string, props = {}): loadResult => {
// 查找元素是否被加载过
const css = document.querySelector(`link[href="${path}"]`);
if (!css) {
const newCss = document.createElement('link');
newCss.rel = 'stylesheet';
newCss.href = path;
Object.keys(props).forEach((key) => {
newCss[key] = props[key];
});
// IE9+
const isLegacyIECss = 'hideFocus' in newCss;
// 防止 ie 中的错误
// use preload in IE Edge (to detect load errors)
if (isLegacyIECss && newCss.relList) {
newCss.rel = 'preload';
newCss.as = 'style';
}
newCss.setAttribute('data-status', 'loading');
document.head.appendChild(newCss);
return {
ref: newCss,
status: 'loading',
};
}
return {
ref: css,
status: (css.getAttribute('data-status') as Status) || 'ready',
};
};
const useExternal = (path?: string, options?: Options) => {
const [status, setStatus] = useState<Status>(path ? 'loading' : 'unset');
const ref = useRef<Element>();
useEffect(() => {
if (!path) {
setStatus('unset');
return;
}
const pathname = path.replace(/[|#].*$/, '');
// 推断类型
if (options?.type === 'css' || (!options?.type && /(^css!|.css$)/.test(pathname))) {
const result = loadCss(path, options?.css);
ref.current = result.ref;
setStatus(result.status);
} else if (options?.type === 'js' || (!options?.type && /(^js!|.js$)/.test(pathname))) {
const result = loadScript(path, options?.js);
ref.current = result.ref;
setStatus(result.status);
} else {
// do nothing
console.error(
"Cannot infer the type of external resource, and please provide a type ('js' | 'css'). " +
'Refer to the https://ahooks.js.org/hooks/dom/use-external/#options',
);
}
if (!ref.current) {
return;
}
// 加载计数
if (EXTERNAL_USED_COUNT[path] === undefined) {
EXTERNAL_USED_COUNT[path] = 1;
} else {
EXTERNAL_USED_COUNT[path] += 1;
}
const handler = (event: Event) => {
const targetStatus = event.type === 'load' ? 'ready' : 'error';
ref.current?.setAttribute('data-status', targetStatus);
setStatus(targetStatus);
};
// 设置加载成功和失败的函数
ref.current.addEventListener('load', handler);
ref.current.addEventListener('error', handler);
return () => {
// 移除监听函数
ref.current?.removeEventListener('load', handler);
ref.current?.removeEventListener('error', handler);
EXTERNAL_USED_COUNT[path] -= 1;
// 如果引用计数为0 ,移除元素
if (EXTERNAL_USED_COUNT[path] === 0) {
ref.current?.remove();
}
ref.current = undefined;
};
}, [path]);
return status;
};
useTitle
const DEFAULT_OPTIONS: Options = {
restoreOnUnmount: false,
};
function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
// ref 保存上一次标题,兼容ssr
const titleRef = useRef(isBrowser ? document.title : '');
useEffect(() => {
document.title = title;
}, [title]);
useUnmount(() => {
// 卸载时是否需要恢复原来的标题
if (options.restoreOnUnmount) {
document.title = titleRef.current;
}
});
}
useFavicon
const ImgTypeMap = {
SVG: 'image/svg+xml',
ICO: 'image/x-icon',
GIF: 'image/gif',
PNG: 'image/png',
};
type ImgTypes = keyof typeof ImgTypeMap;
const useFavicon = (href: string) => {
useEffect(() => {
if (!href) return;
const cutUrl = href.split('.');
// 获取 imgtype
const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes;
// 查找 link 为 icon 或者新建一个 link
const link: HTMLLinkElement =
document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = ImgTypeMap[imgSuffix];
link.href = href;
link.rel = 'shortcut icon';
// 修改 icon
document.getElementsByTagName('head')[0].appendChild(link);
}, [href]);
};
useFullscreen
// 利用 screenfull 实现全屏
import screenfull from 'screenfull';
const useFullscreen = (target: BasicTarget, options?: Options) => {
const { onExit, onEnter } = options || {};
// 离开以及进入的回调
const onExitRef = useLatest(onExit);
const onEnterRef = useLatest(onEnter);
const [state, setState] = useState(false);
const onChange = () => {
// 如果支持全屏
if (screenfull.isEnabled) {
const { isFullscreen } = screenfull;
// 触发监听函数
if (isFullscreen) {
onEnterRef.current?.();
} else {
screenfull.off('change', onChange);
onExitRef.current?.();
}
setState(isFullscreen);
}
};
const enterFullscreen = () => {
const el = getTargetElement(target);
if (!el) {
return;
}
if (screenfull.isEnabled) {
try {
screenfull.request(el);
screenfull.on('change', onChange);
} catch (error) {
console.error(error);
}
}
};
const exitFullscreen = () => {
if (!state) {
return;
}
if (screenfull.isEnabled) {
screenfull.exit();
}
};
const toggleFullscreen = () => {
if (state) {
exitFullscreen();
} else {
enterFullscreen();
}
};
useUnmount(() => {
if (screenfull.isEnabled) {
// 卸载触发监听
screenfull.off('change', onChange);
}
});
return [
state,
{
enterFullscreen: useMemoizedFn(enterFullscreen),
exitFullscreen: useMemoizedFn(exitFullscreen),
toggleFullscreen: useMemoizedFn(toggleFullscreen),
isEnabled: screenfull.isEnabled,
},
] as const;
};
useHover
export default (target: BasicTarget, options?: Options): boolean => {
const { onEnter, onLeave } = options || {};
// 利用 useBoolean hook
const [state, { setTrue, setFalse }] = useBoolean(false);
// 鼠标进入时,触发进入回调,设置状态
useEventListener(
'mouseenter',
() => {
onEnter?.();
setTrue();
},
{
target,
},
);
// 离开时的回调
useEventListener(
'mouseleave',
() => {
onLeave?.();
setFalse();
},
{
target,
},
);
return state;
};
useInViewport
function useInViewport(target: BasicTarget, options?: Options) {
const [state, setState] = useState<boolean>();
const [ratio, setRatio] = useState<number>();
useEffectWithTarget(
() => {
const el = getTargetElement(target);
if (!el) {
return;
}
// 利用 IntersectionObserver 实现
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
setRatio(entry.intersectionRatio);
if (entry.isIntersecting) {
setState(true);
} else {
setState(false);
}
}
},
{
...options,
root: getTargetElement(options?.root),
},
);
// 观察元素
observer.observe(el);
// 停止观察元素
return () => {
observer.disconnect();
};
},
[],
target,
);
return [state, ratio] as const;
}
useKeyPress
function useKeyPress(keyFilter: KeyFilter, eventHandler: EventHandler, option?: Options) {
const { events = defaultEvents, target, exactMatch = false } = option || {};
const eventHandlerRef = useLatest(eventHandler);
const keyFilterRef = useLatest(keyFilter);
useDeepCompareEffectWithTarget(
() => {
const el = getTargetElement(target, window);
if (!el) {
return;
}
const callbackHandler = (event: KeyboardEvent) => {
// 是否满足触发监听函数
const genGuard: KeyPredicate = genKeyFormater(keyFilterRef.current, exactMatch);
if (genGuard(event)) {
return eventHandlerRef.current?.(event);
}
};
// 添加监听函数
for (const eventName of events) {
el?.addEventListener?.(eventName, callbackHandler);
}
return () => {
for (const eventName of events) {
el?.removeEventListener?.(eventName, callbackHandler);
}
};
},
[events],
target,
);
}
useLongPress
// 判断是否支持 touch 事件
const touchSupported =
isBrowser &&
// @ts-ignore
('ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch));
function useLongPress(
onLongPress: (event: EventType) => void,
target: BasicTarget,
{ delay = 300, onClick, onLongPressEnd }: Options = {},
) {
// ref 保存各种值
const onLongPressRef = useLatest(onLongPress);
const onClickRef = useLatest(onClick);
const onLongPressEndRef = useLatest(onLongPressEnd);
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const isTriggeredRef = useRef(false);
useEffectWithTarget(
() => {
const targetElement = getTargetElement(target);
if (!targetElement?.addEventListener) {
return;
}
// 设置 触发长按事件的的定时器, 默认为300ms
const onStart = (event: TouchEvent | MouseEvent) => {
timerRef.current = setTimeout(() => {
onLongPressRef.current(event);
isTriggeredRef.current = true;
}, delay);
};
// 鼠标离开或者抬起时触发函数
const onEnd = (event: TouchEvent | MouseEvent, shouldTriggerClick: boolean = false) => {
// 清除长按监听函数
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// 如果长按被触发,同时触发长按事件的离开函数
if (isTriggeredRef.current) {
onLongPressEndRef.current?.(event);
}
// 是否应该触发点击事件,只有长按事件没有触发的时候,以及存在点击事件的时候
if (shouldTriggerClick && !isTriggeredRef.current && onClickRef.current) {
onClickRef.current(event);
}
isTriggeredRef.current = false;
};
const onEndWithClick = (event: TouchEvent | MouseEvent) => onEnd(event, true);
// 设置各种监听函数
if (!touchSupported) {
targetElement.addEventListener('mousedown', onStart);
targetElement.addEventListener('mouseup', onEndWithClick);
targetElement.addEventListener('mouseleave', onEnd);
} else {
targetElement.addEventListener('touchstart', onStart);
targetElement.addEventListener('touchend', onEndWithClick);
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
isTriggeredRef.current = false;
}
if (!touchSupported) {
targetElement.removeEventListener('mousedown', onStart);
targetElement.removeEventListener('mouseup', onEndWithClick);
targetElement.removeEventListener('mouseleave', onEnd);
} else {
targetElement.removeEventListener('touchstart', onStart);
targetElement.removeEventListener('touchend', onEndWithClick);
}
};
},
[],
target,
);
}
useMouse
export default (target?: BasicTarget) => {
const [state, setState] = useRafState(initState);
// 监听鼠标移动监听函数
useEventListener(
'mousemove',
(event: MouseEvent) => {
const { screenX, screenY, clientX, clientY, pageX, pageY } = event;
const newState = {
screenX,
screenY,
clientX,
clientY,
pageX,
pageY,
elementX: NaN,
elementY: NaN,
elementH: NaN,
elementW: NaN,
elementPosX: NaN,
elementPosY: NaN,
};
const targetElement = getTargetElement(target);
if (targetElement) {
const { left, top, width, height } = targetElement.getBoundingClientRect();
newState.elementPosX = left + window.pageXOffset;
newState.elementPosY = top + window.pageYOffset;
newState.elementX = pageX - newState.elementPosX;
newState.elementY = pageY - newState.elementPosY;
newState.elementW = width;
newState.elementH = height;
}
setState(newState);
},
{
target: () => document,
},
);
return state;
};
useResponsive
let responsiveConfig: ResponsiveConfig = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
};
function handleResize() {
const oldInfo = info;
calculate();
if (oldInfo === info) return;
for (const subscriber of subscribers) {
subscriber();
}
}
let listening = false;
function calculate() {
// 计算响应式信息是否应该更新
const width = window.innerWidth;
const newInfo = {} as ResponsiveInfo;
let shouldUpdate = false;
for (const key of Object.keys(responsiveConfig)) {
newInfo[key] = width >= responsiveConfig[key];
if (newInfo[key] !== info[key]) {
shouldUpdate = true;
}
}
if (shouldUpdate) {
info = newInfo;
}
}
export function configResponsive(config: ResponsiveConfig) {
responsiveConfig = config;
if (info) calculate();
}
export function useResponsive() {
const windowExists = typeof window !== 'undefined';
if (windowExists && !listening) {
info = {};
calculate();
// 设置resize 监听函数
window.addEventListener('resize', handleResize);
listening = true;
}
const [state, setState] = useState<ResponsiveInfo>(info);
useEffect(() => {
if (!windowExists) return;
const subscriber = () => {
setState(info);
};
// 添加订阅函数,方便在 resize 的handleResize 时触发
subscribers.add(subscriber);
return () => {
subscribers.delete(subscriber);
if (subscribers.size === 0) {
window.removeEventListener('resize', handleResize);
listening = false;
}
};
}, []);
return state;
}
useScroll
function useScroll(
target?: Target,
shouldUpdate: ScrollListenController = () => true,
): Position | undefined {
const [position, setPosition] = useRafState<Position>();
const shouldUpdateRef = useLatest(shouldUpdate);
useEffectWithTarget(
() => {
const el = getTargetElement(target, document);
if (!el) {
return;
}
const updatePosition = () => {
let newPosition: Position;
// 区分 document 和其他元素的情况
if (el === document) {
if (document.scrollingElement) {
newPosition = {
left: document.scrollingElement.scrollLeft,
top: document.scrollingElement.scrollTop,
};
} else {
// When in quirks mode, the scrollingElement attribute returns the HTML body element if it exists and is potentially scrollable, otherwise it returns null.
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/scrollingElement
// https://stackoverflow.com/questions/28633221/document-body-scrolltop-firefox-returns-0-only-js
newPosition = {
left: Math.max(
window.pageYOffset,
document.documentElement.scrollTop,
document.body.scrollTop,
),
top: Math.max(
window.pageXOffset,
document.documentElement.scrollLeft,
document.body.scrollLeft,
),
};
}
} else {
newPosition = {
left: (el as Element).scrollLeft,
top: (el as Element).scrollTop,
};
}
// 是否应该更新位置
if (shouldUpdateRef.current(newPosition)) {
setPosition(newPosition);
}
};
updatePosition();
// 滚动监听函数
el.addEventListener('scroll', updatePosition);
return () => {
el.removeEventListener('scroll', updatePosition);
};
},
[],
target,
);
return position;
}
useSize
function useSize(target: BasicTarget): Size | undefined {
const [state, setState] = useRafState<Size>();
// 区分浏览器 和 ssr 情况
useIsomorphicLayoutEffectWithTarget(
() => {
const el = getTargetElement(target);
if (!el) {
return;
}
// 利用 ResizeObserver 进行监听
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { clientWidth, clientHeight } = entry.target;
setState({
width: clientWidth,
height: clientHeight,
});
});
});
resizeObserver.observe(el);
return () => {
// 接触监听
resizeObserver.disconnect();
};
},
[],
target,
);
return state;
}
export default useSize;
useFocusWithin
export default function useFocusWithin(target: BasicTarget, options?: Options) {
const [isFocusWithin, setIsFocusWithin] = useState(false);
const { onFocus, onBlur, onChange } = options || {};
// 利用两个对 target 的 focusin 和 focusout 对焦点事件进行判断
useEventListener(
'focusin',
(e: FocusEvent) => {
if (!isFocusWithin) {
// 触发对焦事件、 改变事件、以及 聚焦状态
onFocus?.(e);
onChange?.(true);
setIsFocusWithin(true);
}
},
{
target,
},
);
// 跟上面相反,触发失焦事件 以及失焦状态等
useEventListener(
'focusout',
(e: FocusEvent) => {
// @ts-ignore
if (isFocusWithin && !e.currentTarget?.contains?.(e.relatedTarget)) {
onBlur?.(e);
onChange?.(false);
setIsFocusWithin(false);
}
},
{
target,
},
);
return isFocusWithin;
}
Advanced
useControllableValue
function useControllableValue<T = any>(props: Props = {}, options: Options<T> = {}) {
const {
defaultValue,
defaultValuePropName = 'defaultValue',
valuePropName = 'value',
trigger = 'onChange',
} = options;
const value = props[valuePropName] as T;
// 组件是否受控
const isControlled = props.hasOwnProperty(valuePropName);
// 获取初始化值,受控组件获取 value 值,否则获取默认值,兜底值是传入的值
const initialValue = useMemo(() => {
if (isControlled) {
return value;
}
if (props.hasOwnProperty(defaultValuePropName)) {
return props[defaultValuePropName];
}
return defaultValue;
}, []);
const stateRef = useRef(initialValue);
// 受控组件设置状态Ref 为 value
if (isControlled) {
stateRef.current = value;
}
const update = useUpdate();
function setState(v: SetStateAction<T>, ...args: any[]) {
const r = isFunction(v) ? v(stateRef.current) : v;
// 非受控组件,强制更新值
if (!isControlled) {
stateRef.current = r;
update();
}
// 触发 onChange 事件
if (props[trigger]) {
props[trigger](r, ...args);
}
}
return [stateRef.current, useMemoizedFn(setState)] as const;
}
useCreation
export default function useCreation<T>(factory: () => T, deps: DependencyList) {
// 利用 initialized 是否初始化
const { current } = useRef({
deps,
obj: undefined as undefined | T,
initialized: false,
});
// 监测是否初始化 或者 依赖是否更改,从而保证 memo 不会重复计算
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj as T;
}
useEventEmitter
export class EventEmitter<T> {
private subscriptions = new Set<Subscription<T>>();
emit = (val: T) => {
for (const subscription of this.subscriptions) {
subscription(val);
}
};
useSubscription = (callback: Subscription<T>) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const callbackRef = useRef<Subscription<T>>();
// 防止闭包陷阱
callbackRef.current = callback;
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
function subscription(val: T) {
if (callbackRef.current) {
callbackRef.current(val);
}
}
// 加入监听函数
this.subscriptions.add(subscription);
return () => {
// 卸载
this.subscriptions.delete(subscription);
};
}, []);
};
}
export default function useEventEmitter<T = void>() {
const ref = useRef<EventEmitter<T>>();
if (!ref.current) {
// 创建事件监听
ref.current = new EventEmitter();
}
return ref.current;
}
useIsomorphicLayoutEffect
// ssr 环境下,使用 useEffect
const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect;
export default useIsomorphicLayoutEffect;
useLatest
function useLatest<T>(value: T) {
// 使用 ref 来保存最新值,避免闭包陷阱
const ref = useRef(value);
ref.current = value;
return ref;
}
useMemoizedFn
function useMemoizedFn<T extends noop>(fn: T) {
if (process.env.NODE_ENV === 'development') {
if (!isFunction(fn)) {
console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useRef<T>(fn);
// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
// 这个是为了 devtools 的正常显示
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current) {
// 使用 ref 来避免闭包陷阱
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
useReactive
// k:v 原对象:代理过的对象
const proxyMap = new WeakMap();
// k:v 代理过的对象:原对象
const rawMap = new WeakMap();
function observer<T extends Record<string, any>>(initialVal: T, cb: () => void): T {
const existingProxy = proxyMap.get(initialVal);
// 添加缓存 防止重新构建proxy
if (existingProxy) {
return existingProxy;
}
// 防止代理已经代理过的对象
// https://github.com/alibaba/hooks/issues/839
if (rawMap.has(initialVal)) {
return initialVal;
}
const proxy = new Proxy<T>(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
return isObject(res) ? observer(res, cb) : Reflect.get(target, key);
},
set(target, key, val) {
const ret = Reflect.set(target, key, val);
cb();
return ret;
},
deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key);
cb();
return ret;
},
});
proxyMap.set(initialVal, proxy);
rawMap.set(proxy, initialVal);
return proxy;
}
function useReactive<S extends Record<string, any>>(initialState: S): S {
const update = useUpdate();
const stateRef = useRef<S>(initialState);
const state = useCreation(() => {
return observer(stateRef.current, () => {
// 增删改查进行响应式, 进行强制更新
update();
});
}, []);
return state;
}
Dev
useTrackedEffect
// 返回依赖数组变化的下标值
const diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {
//Let's do a reference equality check on 2 dependency list.
//If deps1 is defined, we iterate over deps1 and do comparison on each element with equivalent element from deps2
//As this func is used only in this hook, we assume 2 deps always have same length.
return deps1
? deps1
.map((_ele, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))
.filter((ele) => ele >= 0)
: deps2
? deps2.map((_ele, idx) => idx)
: [];
};
const useTrackedEffect = (effect: Effect, deps?: DependencyList) => {
// 保存上次的 deps 的值
const previousDepsRef = useRef<DependencyList>();
useEffect(() => {
// 对比 deps 值得变化
const changes = diffTwoDeps(previousDepsRef.current, deps);
const previousDeps = previousDepsRef.current;
// 将当次的依赖放进 ref
previousDepsRef.current = deps;
return effect(changes, previousDeps, deps);
}, deps);
};
export default useTrackedEffect;
useWhyDidYouUpdate
export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
const prevProps = useRef<IProps>({});
useEffect(() => {
if (prevProps.current) {
const allKeys = Object.keys({ ...prevProps.current, ...props });
const changedProps: IProps = {};
// 对比那些 prop 变化了
allKeys.forEach((key) => {
if (!Object.is(prevProps.current[key], props[key])) {
changedProps[key] = {
from: prevProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changedProps).length) {
console.log('[why-did-you-update]', componentName, changedProps);
}
}
// 保存当次的 props ,方便下次更改时进行对比
prevProps.current = props;
});
}
Utils
createEffectWithTarget
与 target 绑定的 effect, 收到进行依赖的判断
const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
/**
*
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
// 初始化标志
const hasInitRef = useRef(false);
// 上一个 元素 ref,
const lastElementRef = useRef<(Element | null)[]>([]);
// 上一个元素依赖
const lastDepsRef = useRef<DependencyList>([]);
const unLoadRef = useRef<any>();
useEffectType(() => {
const targets = Array.isArray(target) ? target : [target];
const els = targets.map((item) => getTargetElement(item));
// init run
if (!hasInitRef.current) {
hasInitRef.current = true;
lastElementRef.current = els;
lastDepsRef.current = deps;
// 保存卸载回调
unLoadRef.current = effect();
return;
}
// 元素判断 以及依赖判断
if (
els.length !== lastElementRef.current.length ||
!depsAreSame(els, lastElementRef.current) ||
!depsAreSame(deps, lastDepsRef.current)
) {
unLoadRef.current?.();
lastElementRef.current = els;
lastDepsRef.current = deps;
unLoadRef.current = effect();
}
});
useUnmount(() => {
unLoadRef.current?.();
// for react-refresh
hasInitRef.current = false;
});
};
return useEffectWithTarget;
};
useDeepCompareEffectWithTarget
const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
return isEqual(aDeps, bDeps);
};
const useDeepCompareEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
// 保存上一次的依赖
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
// 深度依赖变更对比
if (!depsEqual(deps, ref.current)) {
ref.current = deps;
// 更改 sign Ref 的值,让 元素effect 卸载,重新加载一次
signalRef.current += 1;
}
useEffectWithTarget(effect, [signalRef.current], target);
};