
如上图所示,描述了useFusionTable、useAntdTable、usePagination、useRequest四个hook的调用依赖关系。
基于useRequest封装了常用的分页逻辑,针对入参和出参做了处理。
const usePagination = (service, options) => {
const { defaultPageSize = 10, ...rest } = options;
const result = useRequest(service, {
defaultParams: [{ current: 1, pageSize: defaultPageSize }],
refreshDepsAction: () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
changeCurrent(1);
},
...rest,
});
const onChange = (c: number, p: number) => {
let toCurrent = c <= 0 ? 1 : c;
const toPageSize = p <= 0 ? 1 : p;
const tempTotalPage = Math.ceil(total / toPageSize);
if (toCurrent > tempTotalPage) {
toCurrent = Math.max(1, tempTotalPage);
}
const [oldPaginationParams = {}, ...restParams] = result.params || [];
result.run(
{
...oldPaginationParams,
current: toCurrent,
pageSize: toPageSize,
},
...restParams,
);
};
// changeCurrent、changePageSize逻辑
return {
...result,
pagination: {
current,
pageSize,
total,
totalPage,
onChange: useMemoizedFn(onChange), // 页码或 pageSize 改变的回调
changeCurrent: useMemoizedFn(changeCurrent),
changePageSize: useMemoizedFn(changePageSize),
},
} as PaginationResult<TData, TParams>;
};
基于 useRequest 实现,封装了常用的 Ant Design Form 与 Ant Design Table 联动逻辑,并且同时支持 antd v3 和 v4。
const useAntdTable = (service, options) => {
const {
form, //接收form实例,实现Form与Table联动
defaultType = 'simple',
defaultParams,
manual = false,
refreshDeps = [],
ready = true,
...rest
} = options;
const result = usePagination<TData, TParams>(service, {
manual: true,
...rest,
});
const { params = [], run } = result; //params记录缓存的数据
//省略 reset、 submit、 onTableChange 等函数
return {
...result,
tableProps: {
dataSource: result.data?.list || defaultDataSourceRef.current,
loading: result.loading,
onChange: useMemoizedFn(onTableChange),
pagination: {
current: result.pagination.current,
pageSize: result.pagination.pageSize,
total: result.pagination.total,
},
},
search: {
submit: useMemoizedFn(submit),
type,
changeType: useMemoizedFn(changeType),
reset: useMemoizedFn(reset),
},
} as AntdTableResult<TData, TParams>;
};

上面说到在调用hook依赖中用到了useAntdTable,这里比较巧妙的是用到了适配器模式,针对入参和出参进行了适配转换。
const useFusionTable = (service, options): FusionTableResult<TData, TParams> => {
const ret = useAntdTable<TData, TParams>(service, {
...options,
form: options.field ? fieldAdapter(options.field) : undefined,
});
return resultAdapter(ret);
};
// 表单实例适配器,操作表单相关函数做映射
export const fieldAdapter = (field: Field) =>
({
getFieldInstance: (name: string) => field.getNames().includes(name),
setFieldsValue: field.setValues,
getFieldsValue: field.getValues,
resetFields: field.resetToDefault,
validateFields: (fields, callback) => {
field.validate(fields, callback);
},
} as AntdFormUtils);
// 结果适配器,磨平Fusion和Antd差异
export const resultAdapter = (result: any) => {
const tableProps = {
dataSource: result.tableProps.dataSource,
loading: result.tableProps.loading,
onSort: (dataIndex: string, order: string) => {
result.tableProps.onChange(
{ current: result.pagination.current, pageSize: result.pagination.pageSize },
result.params[0]?.filters,
{
field: dataIndex,
order,
},
);
},
onFilter: (filterParams: Object) => {
result.tableProps.onChange(
{ current: result.pagination.current, pageSize: result.pagination.pageSize },
filterParams,
result.params[0]?.sorter,
);
},
};
const paginationProps = {
onChange: result.pagination.changeCurrent,
onPageSizeChange: result.pagination.changePageSize,
current: result.pagination.current,
pageSize: result.pagination.pageSize,
total: result.pagination.total,
};
return {
...result,
tableProps,
paginationProps,
};
};
封装了常见的无限滚动逻辑,会自动帮忙整合多次请求的数据结果。
const scrollMethod = () => {
const el = getTargetElement(target);
if (!el) {
return;
}
// 元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。
const scrollTop = getScrollTop(el);
// 元素内容高度的度量
const scrollHeight = getScrollHeight(el);
// 元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。
const clientHeight = getClientHeight(el);
if (scrollHeight - scrollTop <= clientHeight + threshold) {
loadMore();
}
};
useEventListener(
'scroll',
() => {
if (loading || loadingMore) {
return;
}
scrollMethod();
},
{ target },
);

管理动态列表状态,并能生成唯一 key 。
const useDynamicList = <T>(initialList: T[] = []) => {
// 计数器
const counterRef = useRef(-1);
// 唯一key列表
const keyList = useRef<number[]>([]);
const setKey = useCallback((index: number) => {
counterRef.current += 1;
keyList.current.splice(index, 0, counterRef.current);
}, []);
// 基于入参List,记录数量和keyList。
const [list, setList] = useState(() => {
initialList.forEach((_, index) => {
setKey(index);
});
return initialList;
});
const push = useCallback((item: T) => {
setList((l) => {
setKey(l.length);
return l.concat([item]);
});
}, []);
const pop = useCallback(() => {
// remove keys if necessary
try {
keyList.current = keyList.current.slice(0, keyList.current.length - 1);
} catch (e) {
console.error(e);
}
setList((l) => l.slice(0, l.length - 1));
}, []);
}
提供虚拟化列表能力的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。


const useVirtualList = <T = any>(list: T[], options: Options<T>) => { const { const useVirtualList = <T = any>(list: T[], options: Options<T>) => {
const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
const totalHeight = useMemo(() => {
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
// @ts-ignore
// Item 高度不一致时,计算逻辑
return list.reduce((sum, _, index) => sum + itemHeightRef.current(index, list[index]), 0);
}, [list]);
// 计算需要渲染Item的索引范围
const calculateRange = () => {
const container = getTargetElement(containerTarget); // 可视区域
const wrapper = getTargetElement(wrapperTarget); // 可滚动区域
if (container && wrapper) {
const { scrollTop, clientHeight } = container;
const offset = getOffset(scrollTop); // 顶部偏移的Item数
const visibleCount = getVisibleCount(clientHeight, offset); //可见Item数
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]);
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; // 标记位,避免二次触发scroll事件
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
return [targetList, useMemoizedFn(scrollTo)] as const;
};
export default useVirtualList;
管理状态历史变化记录,方便在历史记录中前进与后退。例如撤销,恢复的场景。
如下表格展示了Input输入后状态的变化,以及前进,回退操作后状态变化。
| input初始值 | present: undefined, past: [], future: [] |
|---|---|
| 输入1 | present: “1”, past: [undefined], future: [] |
| 追加输入2 | present: “12”, past: [undefined, “1”], future: [] |
| 追加输入3 | present: “123”, past: [undefined, “1”, “12”], future: [] |
| 回退 | present: “12”, past: [undefined, “1”], future: [“123”] |
| 回退 | present: “1”, past: [undefined], future: [“12”, “123”] |
| 前进 | present: “12”, past: [undefined, “1”], future: [“123”] |
export default function useHistoryTravel<T>(initialValue?: T) {
const [history, setHistory] = useState<IData<T | undefined>>({
present: initialValue, // 记录当前值
past: [], // 记录过去的值
future: [], // 记录未来的值
});
const updateValue = (val: T) => {
setHistory({
present: val,
future: [],
past: [...past, present], // 更新当前值,把上一次值追加到past数组中
});
};
const _backward = (step: number = -1) => {
if (past.length === 0) {
return;
}
const { _before, _current, _after } = split(step, past);
console.log(_before, _current, _after);
setHistory({
past: _before,
present: _current,
future: [..._after, present, ...future],
});
};
}
管理网络连接状态。监听online 、 offline、 change事件。初始网络状态通过navigator.onLine获取
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;
}
联动 Checkbox 逻辑封装,支持多选,单选,全选逻辑,还提供了是否选择,是否全选,是否半选的状态。
export default function useSelections<T>(items: T[], defaultSelected: T[] = []) {
const [selected, setSelected] = useState<T[]>(defaultSelected);
// 这里将原始数据转为Set数组,采用has方法方便判断唯一性。例如是对象数组,同样支持
const selectedSet = useMemo(() => new Set(selected), [selected]);
// 判断是否选中
const isSelected = (item: T) => selectedSet.has(item);
// 选择
const select = (item: T) => {
selectedSet.add(item);
return setSelected(Array.from(selectedSet));
};
// 取消选择
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));
}
// 省略n行代码
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;
}
范例:
const result: Result= useSelections<T>(items: T[], defaultSelected?: T[]);
useSelections([1, 2, 3, 4, 5, 6, 7, 8]);
useSelections([{ value: 1, label: "苹果" }, { value: 2, label: "梨" }]);
用于管理倒计时的 Hook。
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) => {
// targetDate 截止时间
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);
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;
};
管理计数器的 Hook。
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,
});
});
};
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;
}
实时获取用户当前选取的文本内容及位置。
export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
if (oldDeps === deps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false;
}
return true;
}
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);
const lastElementRef = useRef<(Element | null)[]>([]);
const lastDepsRef = useRef<DependencyList>([]);
const unLoadRef = useRef<any>();
useEffectType(() => {
const targets = 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;
};
用于处理 WebSocket 的 Hook
export default function useWebSocket(socketUrl: string, options: Options = {}): Result {
const {
reconnectLimit = 3,
reconnectInterval = 3 * 1000,
manual = false,
onOpen,
onClose,
onMessage,
onError,
protocols,
} = options;
// 返回当前最新值的 Hook,可以避免闭包问题。
const onOpenRef = useLatest(onOpen);
const onCloseRef = useLatest(onClose);
const onMessageRef = useLatest(onMessage);
const onErrorRef = useLatest(onError);
const reconnectTimesRef = useRef(0);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>();
const websocketRef = useRef<WebSocket>();
const unmountedRef = useRef(false);
const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message']>();
const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);
// error, close 情况下进行重新连接
const reconnect = () => {
if (
reconnectTimesRef.current < reconnectLimit &&
websocketRef.current?.readyState !== ReadyState.Open
) {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
reconnectTimerRef.current = setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
connectWs();
console.log(2);
reconnectTimesRef.current++;
}, reconnectInterval);
}
};
const connectWs = () => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
if (websocketRef.current) {
websocketRef.current.close();
}
const ws = new WebSocket(socketUrl, protocols);
setReadyState(ReadyState.Connecting);
ws.onerror = (event) => {
if (unmountedRef.current) {
return;
}
console.log(event);
reconnect();
onErrorRef.current?.(event, ws);
setReadyState(ws.readyState || ReadyState.Closed);
};
ws.onopen = (event) => {
if (unmountedRef.current) {
return;
}
onOpenRef.current?.(event, ws);
reconnectTimesRef.current = 0;
setReadyState(ws.readyState || ReadyState.Open);
};
ws.onmessage = (message: WebSocketEventMap['message']) => {
if (unmountedRef.current) {
return;
}
onMessageRef.current?.(message, ws);
setLatestMessage(message);
console.log(message);
};
ws.onclose = (event) => {
console.log(event);
if (unmountedRef.current) {
return;
}
reconnect();
onCloseRef.current?.(event, ws);
setReadyState(ws.readyState || ReadyState.Closed);
};
websocketRef.current = ws;
};
const sendMessage: WebSocket['send'] = (message) => {
if (readyState === ReadyState.Open) {
websocketRef.current?.send(message);
} else {
throw new Error('WebSocket disconnected');
}
};
const connect = () => {
reconnectTimesRef.current = 0;
connectWs();
};
const disconnect = () => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
reconnectTimesRef.current = reconnectLimit;
websocketRef.current?.close();
};
useEffect(() => {
if (!manual) {
connect();
}
}, [socketUrl, manual]);
useUnmount(() => {
unmountedRef.current = true;
disconnect();
});
return {
latestMessage,
sendMessage: useMemoizedFn(sendMessage),
connect: useMemoizedFn(connect),
disconnect: useMemoizedFn(disconnect),
readyState,
webSocketIns: websocketRef.current,
};
}