useUpdateEffect
useUpdateEffect用法等同于useEffect,但是会忽略首次执行,只在依赖更新时执行。
import { useEffect, useRef } from 'react';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isMounted = useRef(false);
// 首次渲染后标记为已挂载
useEffect(() => {
isMounted.current = true;
// 组件卸载时重置标志
return () => {
isMounted.current = false;
};
}, []);
// 实际的 effect,只有在已挂载且依赖项变化时才执行
useEffect(() => {
if (isMounted.current) {
return effect();
}
}, deps);
};
export default useUpdateEffect;
useUpdateLayoutEffect
useUpdateLayoutEffect用法等同于useLayoutEffect,但是会忽略首次执行,只在依赖更新时执行。
import { useLayoutEffect, useRef } from 'react';
const useUpdateLayoutEffect: typeof useLayoutEffect = (effect, deps) => {
const isMounted = useRef(false);
// 首次渲染后标记为已挂载
useLayoutEffect(() => {
isMounted.current = true;
// 组件卸载时重置标志
return () => {
isMounted.current = false;
};
}, []);
// 实际的 layout effect,只有在已挂载且依赖项变化时才执行
useLayoutEffect(() => {
if (isMounted.current) {
return effect();
}
}, deps);
};
export default useUpdateLayoutEffect;
// 使用场景:DOM 同步测量和调整
import { useUpdateLayoutEffect } from 'ahooks';
function ResponsiveComponent({ items }) {
const containerRef = useRef(null);
// 需要在 DOM 更新后立即测量并调整样式
useUpdateLayoutEffect(() => {
const container = containerRef.current;
if (container) {
// 测量容器尺寸
const { width } = container.getBoundingClientRect();
// 根据宽度调整子元素样式(需同步执行避免闪烁)
const children = container.querySelectorAll('.item');
children.forEach(child => {
child.style.flexBasis = width > 600 ? '50%' : '100%';
});
}
}, [items]);
return (
<div ref={containerRef}>
{items.map(item => (
<div key={item.id} className="item">
{item.content}
</div>
))}
</div>
);
}
useAsyncEffect
useEffect 支持异步函数
import { useEffect, useRef } from 'react';
const useAsyncEffect = (
effect: (isCanceled: () => boolean) => Promise<void | (() => void)>,
deps?: React.DependencyList
) => {
const isCanceledRef = useRef(false);
useEffect(() => {
isCanceledRef.current = false;
// 执行异步效应
const result = effect(() => isCanceledRef.current);
// 返回清理函数
return () => {
isCanceledRef.current = true;
// 如果 effect 返回了 Promise,处理其结果
if (result instanceof Promise) {
result.then(cleanup => {
if (cleanup && !isCanceledRef.current) {
cleanup();
}
});
}
};
}, deps);
};
export default useAsyncEffect;
// 使用场景:异步数据获取
import { useAsyncEffect } from 'ahooks';
import { useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useAsyncEffect(async (isCanceled) => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
// 检查是否已取消,避免在组件卸载后设置状态
if (!isCanceled()) {
setUser(userData);
setLoading(false);
}
} catch (error) {
if (!isCanceled()) {
console.error('Failed to fetch user:', error);
setLoading(false);
}
}
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>Hello, {user?.name}!</div>;
}
useDebounceFn
用来处理防抖函数的 Hook
import { useMemo, useRef } from 'react';
import debounce from 'lodash/debounce';
import type { DebounceSettings } from 'lodash';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
export interface DebounceOptions extends DebounceSettings {
wait?: number;
}
type Fn = (...args: any[]) => any;
function useDebounceFn<T extends Fn>(fn: T, options?: DebounceOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debouncedFn = useMemo(
() =>
debounce(
((...args: any[]) => {
return fnRef.current(...args);
}) as T,
wait,
options,
),
[],
);
useUnmount(() => {
debouncedFn.cancel();
});
return [debouncedFn, debouncedFn.cancel, debouncedFn.flush] as const;
}
export default useDebounceFn;
// 基本用法
const [debouncedFn, cancel, flush] = useDebounceFn(
(value) => {
console.log('防抖执行:', value);
},
{ wait: 500 }
);
// 输入防抖
const handleChange = (e) => {
debouncedFn(e.target.value);
};
// 手动取消
const handleCancel = () => {
cancel();
};
// 立即执行
const handleFlush = () => {
flush();
};
useDebounceEffect
为
useEffect增加防抖的能力
import { useEffect } from 'react';
import useDebounceFn from '../useDebounceFn';
import type { DebounceOptions } from '../useDebounceFn';
function useDebounceEffect(
effect: React.EffectCallback,
deps?: React.DependencyList,
options?: DebounceOptions,
) {
const [run] = useDebounceFn(effect, options);
useEffect(() => {
run();
}, deps);
}
export default useDebounceEffect;
// 使用场景:窗口尺寸变化防抖
useDebounceEffect(
() => {
console.log('Window resized');
// 执行相关操作
},
[windowWidth, windowHeight],
{ wait: 300 }
);
useThrottleFn
用来处理函数节流的 Hook
import { useMemo } from 'react';
import throttle from 'lodash/throttle';
import type { ThrottleSettings } from 'lodash';
import useLatest from '../useLatest';
import useUnmount from '../useUnmount';
export interface ThrottleOptions extends ThrottleSettings {
wait?: number;
}
type Fn = (...args: any[]) => any;
function useThrottleFn<T extends Fn>(fn: T, options?: ThrottleOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttledFn = useMemo(
() =>
throttle(
((...args: any[]) => {
return fnRef.current(...args);
}) as T,
wait,
options,
),
[],
);
useUnmount(() => {
throttledFn.cancel();
});
return [throttledFn, throttledFn.cancel, throttledFn.flush] as const;
}
export default useThrottleFn;
// 基本用法
const [throttledFn, cancel, flush] = useThrottleFn(
(value) => {
console.log('Throttled:', value);
},
{ wait: 500 }
);
// 滚动事件节流
const handleScroll = (e) => {
throttledFn(e.target.scrollTop);
};
// 取消节流
const handleCancel = () => {
cancel();
};
// 立即执行
const handleFlush = () => {
flush();
};
useThrottleEffect
import { useEffect } from 'react';
import useThrottleFn from '../useThrottleFn';
import type { ThrottleOptions } from '../useThrottleFn';
function useThrottleEffect(
effect: React.EffectCallback,
deps?: React.DependencyList,
options?: ThrottleOptions,
) {
const [run] = useThrottleFn(effect, options);
useEffect(() => {
run();
}, deps);
}
export default useThrottleEffect;
// 使用场景:防止频繁的搜索请求
useThrottleEffect(
() => {
if (searchKeyword) {
fetchSearchResults(searchKeyword);
}
},
[searchKeyword],
{ wait: 500 }
);
useDeepCompareEffect
用法与 useEffect 一致,但 deps 通过 react-fast-compare 进行深比较。
import { useEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
function useDeepCompareEffect(
effect: React.EffectCallback,
deps: React.DependencyList,
) {
const ref = useRef<React.DependencyList>();
const signalRef = useRef<number>(0);
if (!isEqual(ref.current, deps)) {
ref.current = deps;
signalRef.current += 1;
}
useEffect(effect, [signalRef.current]);
}
export default useDeepCompareEffect;
// 对象依赖的深度比较
const userData = { name: 'John', age: 30, address: { city: 'New York' } };
useDeepCompareEffect(() => {
console.log('User data changed:', userData);
// 发送请求或执行其他副作用
}, [userData]); // 即使内容相同但引用不同的对象也会正确比较
// 数组依赖的深度比较
const items = [{ id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' }];
useDeepCompareEffect(() => {
console.log('Items changed:', items);
}, [items]);
useDeepCompareLayoutEffect
用法与 useLayoutEffect 一致,但 deps 通过 react-fast-compare 进行深比较。
import { useLayoutEffect, useRef } from 'react';
import isEqual from 'lodash/isEqual';
function useDeepCompareLayoutEffect(
effect: React.EffectCallback,
deps: React.DependencyList,
) {
const ref = useRef<React.DependencyList>();
const signalRef = useRef<number>(0);
if (!isEqual(ref.current, deps)) {
ref.current = deps;
signalRef.current += 1;
}
useLayoutEffect(effect, [signalRef.current]);
}
export default useDeepCompareLayoutEffect;
// 需要同步执行的 DOM 操作
const styleConfig = { color: 'red', fontSize: 16 };
useDeepCompareLayoutEffect(() => {
// 同步执行,避免页面闪烁
document.getElementById('myElement')?.setAttribute('style', JSON.stringify(styleConfig));
}, [styleConfig]);
// 需要精确控制时机的布局计算
const layoutData = { width: 100, height: 200 };
useDeepCompareLayoutEffect(() => {
// 在浏览器绘制前完成布局计算
calculateAndSetLayout(layoutData);
}, [layoutData]);
useInterval
一个可以处理 setInterval 的 Hook
import { useEffect, useRef } from 'react';
import useLatest from './useLatest';
export interface Options {
immediate?: boolean;
}
function useInterval(
fn: () => void,
delay: number | undefined,
options?: Options,
) {
const fnRef = useLatest(fn);
useEffect(() => {
if (typeof delay !== 'number' || delay < 0) return;
if (options?.immediate) {
fnRef.current();
}
const timer = setInterval(() => {
fnRef.current();
}, delay);
return () => {
clearInterval(timer);
};
}, [delay, options?.immediate]);
}
export default useInterval;
// 使用场景:带立即执行的定时器
useInterval(() => {
console.log('立即执行一次,之后每2秒执行一次');
}, 2000, { immediate: true });
useRafInterval
用
requestAnimationFrame模拟实现setInterval,API 和useInterval保持一致,好处是可以在页面不渲染的时候停止执行定时器,比如页面隐藏或最小化等。
import { useEffect, useRef } from 'react';
import useLatest from '../useLatest';
import { isBrowser } from '../utils/dom';
export interface Options {
immediate?: boolean;
}
function useRafInterval(
fn: () => void,
delay: number | undefined,
options?: Options,
) {
const fnRef = useLatest(fn);
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (!isBrowser) {
return;
}
if (typeof delay !== 'number' || delay < 0) return;
if (options?.immediate) {
fnRef.current();
}
const loop = () => {
timerRef.current = requestAnimationFrame(() => {
fnRef.current();
loop();
});
};
timerRef.current = window.setTimeout(() => {
loop();
}, delay);
return () => {
if (timerRef.current) {
cancelAnimationFrame(timerRef.current);
}
clearTimeout(timerRef.current as unknown as number);
};
}, [delay, options?.immediate]);
const clear = () => {
if (timerRef.current) {
cancelAnimationFrame(timerRef.current);
clearTimeout(timerRef.current as unknown as number);
}
};
return clear;
}
export default useRafInterval;
// 动画更新
function AnimatedComponent() {
const [progress, setProgress] = useState(0);
useRafInterval(() => {
setProgress(p => Math.min(p + 0.01, 1));
}, 16); // 约60fps
return (
<div style={{ width: `${progress * 100}%` }}>
加载中...
</div>
);
}
// 实时数据更新
function RealTimeChart() {
const dataRef = useRef([]);
useRafInterval(() => {
// 更新图表数据
updateChart(dataRef.current);
}, 1000);
return <canvas ref={canvasRef} />;
}
useTimeout
一个可以处理 setTimeout 计时器函数的 Hook
import { useEffect, useRef } from 'react';
import useLatest from './useLatest';
function useTimeout(fn: () => void, delay: number | undefined) {
const fnRef = useLatest(fn);
useEffect(() => {
if (typeof delay !== 'number' || delay < 0) return;
const timer = setTimeout(() => {
fnRef.current();
}, delay);
return () => {
clearTimeout(timer);
};
}, [delay]);
}
export default useTimeout;
// 显示提示信息并在几秒后自动隐藏
const [visible, setVisible] = useState(true);
useTimeout(() => {
setVisible(false);
}, 3000);
// 防止误触的确认操作
const [confirming, setConfirming] = useState(false);
const handleDelete = () => {
if (!confirming) {
setConfirming(true);
useTimeout(() => {
setConfirming(false);
}, 3000);
} else {
// 执行删除操作
performDelete();
}
};
useRafTimeout
用 requestAnimationFrame 模拟实现 setTimeout,API 和 useTimeout 保持一致,好处是可以在页面不渲染的时候不触发函数执行,比如页面隐藏或最小化等。
import { useEffect, useRef } from 'react';
import useLatest from '../useLatest';
import { isBrowser } from '../utils/dom';
function useRafTimeout(fn: () => void, delay: number | undefined) {
const fnRef = useLatest(fn);
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (!isBrowser) {
return;
}
if (typeof delay !== 'number' || delay < 0) return;
const loop = () => {
timerRef.current = requestAnimationFrame(() => {
fnRef.current();
});
};
timerRef.current = window.setTimeout(() => {
loop();
}, delay);
return () => {
if (timerRef.current) {
cancelAnimationFrame(timerRef.current);
}
clearTimeout(timerRef.current as unknown as number);
};
}, [delay]);
const clear = () => {
if (timerRef.current) {
cancelAnimationFrame(timerRef.current);
clearTimeout(timerRef.current as unknown as number);
}
};
return clear;
}
export default useRafTimeout;
// 基本用法
const clear = useRafTimeout(() => {
console.log('在下一动画帧执行');
}, 1000);
// 动画相关操作
useRafTimeout(() => {
// 在浏览器重绘前执行动画更新
updateAnimationState();
}, 16); // 约一帧的时间
// 手动清除
const clearTimeout = useRafTimeout(() => {
performAction();
}, 3000);
// 在需要时清除
const handleCancel = () => {
clearTimeout();
};
useLockFn
用于给一个异步函数增加竞态锁,防止并发执行。
import { useRef, useCallback } from 'react';
function useLockFn<P extends any[] = any[], V extends any = any>(
fn: (...args: P) => Promise<V> | V,
) {
const lockRef = useRef(false);
return useCallback(
async (...args: P) => {
if (lockRef.current) return;
lockRef.current = true;
try {
const ret = await fn(...args);
return ret;
} catch (e) {
throw e;
} finally {
lockRef.current = false;
}
},
[fn],
);
}
export default useLockFn;
// 防止重复提交
const submit = useLockFn(async (data) => {
const result = await api.submit(data);
return result;
});
// 防止重复请求
const fetchData = useLockFn(async (id) => {
const data = await api.getData(id);
setData(data);
return data;
});
// 处理用户连续点击
const handleClick = useLockFn(async () => {
setLoading(true);
try {
await performAsyncOperation();
} finally {
setLoading(false);
}
});
useUpdate
useUpdate 会返回一个函数,调用该函数会强制组件重新渲染
import { useCallback, useState } from 'react';
function useUpdate() {
const [, setState] = useState({});
return useCallback(() => setState({}), []);
}
export default useUpdate;
// 基本用法
function MyComponent() {
const update = useUpdate();
const handleClick = () => {
// 执行某些不改变状态的操作
doSomething();
// 强制组件重新渲染
update();
};
return (
<div>
<p>当前时间: {Date.now()}</p>
<button onClick={handleClick}>强制更新</button>
</div>
);
}