📝 解读 ahooks 源码系列 - Scene
目录
本篇文章是解读 ahooks@3.8.0 源码系列的第二篇 - Scene,欢迎您的指正和点赞。
本文主要解读 useAntdTable、useFusionTable、useInfiniteScroll、usePagination、useDynamicList、useVirtualList、useHistoryTravel、useNetwork、useSelections、useCountDown、useCounter、useTextSelection、useWebSocket 的源码实现。
useAntdTable
import type {
PaginationOptions,
PaginationResult,
} from "../usePagination/types";
export type Data = { total: number; list: any[] };
export type Params = [
{
current: number;
pageSize: number;
sorter?: any;
filter?: any;
extra?: any;
[key: string]: any;
},
...any[]
];
export type Service<TData extends Data, TParams extends Params> = (
...args: TParams
) => Promise<TData>;
export type Antd3ValidateFields = (
fieldNames: string[],
callback: (errors, values: Record<string, any>) => void
) => void;
export type Antd4ValidateFields = (
fieldNames?: string[]
) => Promise<Record<string, any>>;
export interface AntdFormUtils {
getFieldInstance?: (name: string) => Record<string, any>;
setFieldsValue: (value: Record<string, any>) => void;
getFieldsValue: (...args: any) => Record<string, any>;
resetFields: (...args: any) => void;
validateFields: Antd3ValidateFields | Antd4ValidateFields;
getInternalHooks?: any;
[key: string]: any;
}
export interface AntdTableResult<TData extends Data, TParams extends Params>
extends PaginationResult<TData, TParams> {
tableProps: {
dataSource: TData["list"];
loading: boolean;
onChange: (pagination: any, filters?: any, sorter?: any) => void;
pagination: any;
[key: string]: any;
};
search: {
type: "simple" | "advance";
changeType: () => void;
submit: () => void;
reset: () => void;
};
}
export interface AntdTableOptions<TData extends Data, TParams extends Params>
extends PaginationOptions<TData, TParams> {
form?: AntdFormUtils;
defaultType?: "simple" | "advance";
}
import { useEffect, useRef, useState } from "react";
import useMemoizedFn from "../useMemoizedFn";
import usePagination from "../usePagination";
import useUpdateEffect from "../useUpdateEffect";
import type {
Antd4ValidateFields,
AntdTableOptions,
AntdTableResult,
Data,
Params,
Service,
} from "./types";
const useAntdTable = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: AntdTableOptions<TData, TParams> = {}
) => {
const {
// form 实例
form,
// 默认表单类型
defaultType = "simple",
// 默认参数,第一项为分页数据,第二项为表单数据
defaultParams,
manual = false,
refreshDeps = [],
ready = true,
...rest
} = options;
// 分页
const result = usePagination<TData, TParams>(service, {
manual: true,
...rest,
onSuccess(...args) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
runSuccessRef.current = true;
rest.onSuccess?.(...args);
},
});
const { params = [], run } = result;
const cacheFormTableData = params[2] || ({} as any);
const [type, setType] = useState(cacheFormTableData?.type || defaultType);
const allFormDataRef = useRef<Record<string, any>>({});
const defaultDataSourceRef = useRef([]);
const runSuccessRef = useRef(false);
// 判断是否为 antd v4
const isAntdV4 = !!form?.getInternalHooks;
// get current active field values
// 获取表单值
const getActiveFieldValues = () => {
if (!form) {
return {};
}
// antd v4
if (isAntdV4) {
return form.getFieldsValue(null, () => true);
}
// antd v3
const allFieldsValue = form.getFieldsValue();
const activeFieldsValue = {};
Object.keys(allFieldsValue).forEach((key: string) => {
if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
activeFieldsValue[key] = allFieldsValue[key];
}
});
return activeFieldsValue;
};
// 校验表单
const validateFields = (): Promise<Record<string, any>> => {
if (!form) {
return Promise.resolve({});
}
const activeFieldsValue = getActiveFieldValues();
const fields = Object.keys(activeFieldsValue);
// antd v4
if (isAntdV4) {
return (form.validateFields as Antd4ValidateFields)(fields);
}
// antd v3
return new Promise((resolve, reject) => {
form.validateFields(fields, (errors, values) => {
if (errors) {
reject(errors);
} else {
resolve(values);
}
});
});
};
// 重置表单
const restoreForm = () => {
if (!form) {
return;
}
// antd v4
if (isAntdV4) {
return form.setFieldsValue(allFormDataRef.current);
}
// antd v3
const activeFieldsValue = {};
Object.keys(allFormDataRef.current).forEach((key) => {
if (form.getFieldInstance ? form.getFieldInstance(key) : true) {
activeFieldsValue[key] = allFormDataRef.current[key];
}
});
form.setFieldsValue(activeFieldsValue);
};
// 修改表单类型
const changeType = () => {
// 获取表单值
const activeFieldsValue = getActiveFieldValues();
// 修改表单值
allFormDataRef.current = {
...allFormDataRef.current,
...activeFieldsValue,
};
setType((t) => (t === "simple" ? "advance" : "simple"));
};
// change search type, restore form data
// 修改 type,重置 form 表单数据
useUpdateEffect(() => {
if (!ready) {
return;
}
restoreForm();
}, [type]);
const _submit = (initPagination?: TParams[0]) => {
if (!ready) {
return;
}
setTimeout(() => {
// 表单校验
validateFields()
.then((values = {}) => {
// 分页逻辑
const pagination = initPagination || {
pageSize: options.defaultPageSize || 10,
...(params?.[0] || {}),
current: 1,
};
// 如果没有 form,直接根据分页逻辑进行请求
if (!form) {
// @ts-ignore
run(pagination);
return;
}
// 获取到当前所有的 form Data
// record all form data
allFormDataRef.current = {
...allFormDataRef.current,
...values,
};
// @ts-ignore
run(pagination, values, {
allFormData: allFormDataRef.current,
type,
});
})
.catch((err) => err);
});
};
// 重置表单
const reset = () => {
if (form) {
form.resetFields();
}
_submit({
...(defaultParams?.[0] || {}),
pageSize:
options.defaultPageSize || options.defaultParams?.[0]?.pageSize || 10,
current: 1,
});
};
// 提交表单
const submit = (e?: any) => {
e?.preventDefault?.();
_submit(
runSuccessRef.current
? undefined
: {
pageSize:
options.defaultPageSize ||
options.defaultParams?.[0]?.pageSize ||
10,
current: 1,
...(defaultParams?.[0] || {}),
}
);
};
// 分页、排序、筛选变化时触发
const onTableChange = (
pagination: any,
filters: any,
sorter: any,
extra: any
) => {
const [oldPaginationParams, ...restParams] = params || [];
run(
// @ts-ignore
{
...oldPaginationParams,
current: pagination.current,
pageSize: pagination.pageSize,
filters,
sorter,
extra,
},
...restParams
);
};
// init
useEffect(() => {
// if has cache, use cached params. ignore manual and ready.
if (params.length > 0) {
allFormDataRef.current = cacheFormTableData?.allFormData || {};
restoreForm();
// @ts-ignore
run(...params);
return;
}
if (!manual && ready) {
allFormDataRef.current = defaultParams?.[1] || {};
restoreForm();
_submit(defaultParams?.[0]);
}
}, []);
// refresh & ready change on the same time
const hasAutoRun = useRef(false);
hasAutoRun.current = false;
// ready 状态变化时的副作用
useUpdateEffect(() => {
if (!manual && ready) {
hasAutoRun.current = true;
if (form) {
form.resetFields();
}
allFormDataRef.current = defaultParams?.[1] || {};
restoreForm();
_submit(defaultParams?.[0]);
}
}, [ready]);
// 依赖项变化时的副作用
useUpdateEffect(() => {
if (hasAutoRun.current) {
return;
}
if (!ready) {
return;
}
if (!manual) {
hasAutoRun.current = true;
result.pagination.changeCurrent(1);
}
}, [...refreshDeps]);
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>;
};
export default useAntdTable;
useFusionTable
文档地址:https://ahooks.js.org/zh-CN/hooks/use-fusion-table
详细代码:https://github.com/alibaba/hooks/tree/master/packages/hooks/src/useFusionTable
useInfiniteScroll
import type { DependencyList } from "react";
import type { BasicTarget } from "utils/domTarget";
export type Data = { list: any[]; [key: string]: any };
export type Service<TData extends Data> = (
currentData?: TData
) => Promise<TData>;
export interface InfiniteScrollResult<TData extends Data> {
Data: TData;
loading: boolean;
loadingMore: boolean;
error?: Error;
noMore: boolean;
loadMore: () => void;
loadMoreAsync: () => Promise<TData>;
reload: () => void;
reloadAsync: () => Promise<TData>;
cancel: () => void;
mutate: (data?: TData) => void;
}
export interface InfiniteScrollOptions<TData extends Data> {
target?: BasicTarget<Element | Document>;
isNoMore?: (data?: TData) => boolean;
threshold?: number;
manual?: boolean;
reloadDeps?: DependencyList;
onBefore?: () => void;
onSuccess?: (data: TData) => void;
onError?: (e: Error) => void;
onFinally?: (data?: TData, e?: Error) => void;
}
/**
* scrollTop: 表示一个元素的垂直滚动条滚动的距离
* scrollHeight: 表示一个元素的内容的总高度,包括不可见部分
* clientHeight: 表示一个元素在视窗中可见部分的高度
*/
const getScrollTop = (el: Document | Element) => {
if (
el === document ||
el === document.documentElement ||
el === document.body
) {
return Math.max(
window.pageYOffset,
document.documentElement.scrollTop,
document.body.scrollTop
);
}
return (el as Element).scrollTop;
};
const getScrollHeight = (el: Document | Element) => {
return (
(el as Element).scrollHeight ||
Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)
);
};
const getClientHeight = (el: Document | Element) => {
return (
(el as Element).clientHeight ||
Math.max(document.documentElement.clientHeight, document.body.clientHeight)
);
};
export { getClientHeight, getScrollHeight, getScrollTop };
import { useMemo, useState } from "react";
import type { Data, InfiniteScrollOptions, Service } from "./types";
import useRequest from "../useRequest";
import useMemoizedFn from "../useMemoizedFn";
import useUpdateEffect from "../useUpdateEffect";
import useEventListener from "../useEventListener";
import { getTargetElement } from "utils/domTarget";
import { getClientHeight, getScrollHeight, getScrollTop } from "utils/rect";
const useInfiniteScroll = <TData extends Data>(
service: Service<TData>,
options: InfiniteScrollOptions<TData> = {}
) => {
const {
// 父级容器
target,
// 是否有最后一页的判断逻辑
isNoMore,
// 下拉自动加载,距离底部距离阈值
threshold = 100,
// 变化后,会自动触发 reload
reloadDeps = [],
manual,
onBefore,
onSuccess,
onError,
onFinally,
} = options;
// 聚合后的数据
const [finalData, setFinalData] = useState<TData>();
// 加载更多 loading
const [loadingMore, setLoadingMore] = useState(false);
const { loading, error, run, runAsync, cancel } = useRequest(
// 入参,将上次请求返回的数据整合到新的参数中
async (lastData?: TData) => {
const currentData = await service(lastData);
// 首次请求,直接设置
if (!lastData) {
setFinalData({
...currentData,
list: [...(currentData.list ?? [])],
});
} else {
setFinalData({
...currentData,
list: [...(lastData.list ?? []), ...currentData.list],
});
}
return currentData;
},
{
manual,
onFinally: (_, d, e) => {
// 设置 loadingMore 为 false
setLoadingMore(false);
onFinally?.(d, e);
},
onBefore: () => onBefore?.(),
onSuccess: (d) => {
setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
scrollMethod();
});
onSuccess?.(d);
},
onError: (e) => onError?.(e),
}
);
// 是否没有更多数据了,配置 options.isNoMore 后生效
const noMore = useMemo(() => {
if (!isNoMore) return false;
return isNoMore(finalData);
}, [finalData]);
// 同步加载更多
const loadMore = useMemoizedFn(() => {
if (noMore) return;
setLoadingMore(true);
run(finalData);
});
// 异步加载更多
const loadMoreAsync = useMemoizedFn(() => {
if (noMore) return Promise.reject();
setLoadingMore(true);
return runAsync(finalData);
});
// 同步加载第一页数据
const reload = () => {
setLoadingMore(false);
return run();
};
// 异步加载第一页数据
const reloadAsync = () => {
setLoadingMore(false);
return runAsync();
};
// 监听 reloadDeps,变化后,自动触发 reload
useUpdateEffect(() => {
run();
}, [...reloadDeps]);
// 滚动
const scrollMethod = () => {
let el = getTargetElement(target);
if (!el) {
return;
}
el = el === document ? document.documentElement : el;
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 }
);
return {
data: finalData,
loaading: !loadMore && loading,
loadingMore,
error,
noMore,
loadMore,
loadMoreAsync,
reload: useMemoizedFn(reload),
reloadAsync: useMemoizedFn(reloadAsync),
mutate: setFinalData,
cancel,
};
};
export default useInfiniteScroll;
usePagination
import type { Result, Options } from "../useRequest/src/types";
export type Data = { total: number; list: any[] };
export type Params = [
{ current: number; pageSize: number; [key: string]: any },
...any[]
];
export type Service<TData extends Data, TParams extends Params> = (
...args: TParams
) => Promise<TData>;
export interface PaginationResult<TData extends Data, TParams extends Params>
extends Result<TData, TParams> {
pagination: {
current: number;
pageSize: number;
total: number;
totalPage: number;
onChange: (current: number, pageSize: number) => void;
changeCurrent: (current: number) => void;
changePageSize: (pageSize: number) => void;
};
}
export interface PaginationOptions<TData extends Data, TParams extends Params>
extends Options<TData, TParams> {
defaultPageSize?: number;
defaultCurrent?: number;
}
import { useMemo } from "react";
import useMemoizedFn from "@/hooks/useMemoizedFn";
import useRequest from "@/hooks/useRequest";
import type {
Data,
PaginationOptions,
PaginationResult,
Params,
Service,
} from "./types";
/**
* 基于 useRequest,封装了常见的分页逻辑
* */
const usePagination = <TData extends Data, TParams extends Params>(
service: Service<TData, TParams>,
options: PaginationOptions<TData, TParams> = {}
) => {
const { defaultPageSize = 10, defaultCurrent = 1, ...rest } = options;
// // service 约定返回的数据结构为 { total: number, list: Item[] }
const result = useRequest(service, {
// service 的默认参数为 { current: number, pageSize: number }
defaultParams: [{ current: defaultCurrent, pageSize: defaultPageSize }],
// refreshDeps 变化,会重置 current 到第一页,并重新发起请求
refreshDepsAction: () => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
changeCurrent(1);
},
...rest,
});
const { current = 1, pageSize = defaultPageSize } = result.params[0] || {};
// 计算总条数
const total = result.data?.total || 0;
// 计算总页数
const totalPage = useMemo(
() => Math.ceil(total / pageSize),
[pageSize, total]
);
/**
* c: current
* p: pageSize
* */
const onChange = (c: number, p: number) => {
let toCurrent = c < 0 ? 1 : c;
const toPageSize = p <= 0 ? 1 : p;
// 根据 total 和传入的 pageSize 计算当前总页数
const tempTotalPage = Math.ceil(total / toPageSize);
// 如果当前总页数小于当前要跳转的 current,需要将 current 赋值为当前总页数
if (toCurrent > tempTotalPage) {
toCurrent = Math.max(1, tempTotalPage);
}
const [oldPaginationParams = {}, ...restParams] = result.params || [];
// 重新执行请求
result.run(
{
...oldPaginationParams,
current: toCurrent,
pageSize: toPageSize,
},
...restParams
);
};
const changeCurrent = (c: number) => {
onChange(c, pageSize);
};
const changePageSize = (p: number) => {
onChange(current, p);
};
// 额外返回 pagination 字段,包含所有分页信息,及操作分页的函数。
return {
...result,
pagination: {
current,
pageSize,
total,
totalPage,
onChange: useMemoizedFn(onChange),
changeCurrent: useMemoizedFn(changeCurrent),
changePageSize: useMemoizedFn(changePageSize),
},
} as PaginationResult<TData, TParams>;
};
export default usePagination;
useDynamicList
/**
* Array.prototype.splice()
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
* const months = ['Jan', 'March', 'April', 'June'];
* months.splice(1, 0, 'Feb'); // 从索引 1 位置开始,不删除元素,插入 ’Feb‘
* // Inserts at index 1
* console.log(months);
* // Expected output: Array ["Jan", "Feb", "March", "April", "June"]
* months.splice(4, 1, 'May'); // 从索引 4 位置开始,删除一个元素,并插入 'May'
* // Replaces 1 element at index 4
* console.log(months);
* // Expected output: Array ["Jan", "Feb", "March", "April", "May"]
*/
import { useCallback, useRef, useState } from "react";
const useDynamicList = <T>(initialList: T[] = []) => {
// 计数
const counterRef = useRef(-1);
// uuid list
const keyList = useRef<number[]>([]);
// 根据 index 为某个元素设置 uuid
const setKey = useCallback((index: number) => {
counterRef.current += 1;
keyList.current.splice(index, 0, counterRef.current);
}, []);
// 当前列表
const [list, setList] = useState(() => {
initialList.forEach((_, index) => {
setKey(index);
});
return initialList;
});
// 重新设置 list 的值
const resetList = useCallback((newList: T[]) => {
keyList.current = [];
setList(() => {
newList.forEach((_, index) => {
setKey(index);
});
return newList;
});
}, []);
// 在指定位置插入元素
const insert = useCallback((index: number, item: T) => {
setList((l) => {
const temp = [...l];
temp.splice(index, 0, item);
setKey(index);
return temp;
});
}, []);
// 获得某个元素的 uuid
const getKey = useCallback((index: number) => keyList.current[index], []);
// 获得某个 key 的index
const getIndex = useCallback(
(key: number) => keyList.current.findIndex((ele) => ele === key),
[]
);
// 在指定位置插入多个元素
const merge = useCallback((index: number, items: T[]) => {
setList((l) => {
const temp = [...l];
items.forEach((_, i) => {
setKey(index + i);
});
temp.splice(index, 0, ...items);
return temp;
});
}, []);
// 替换指定元素
const replace = useCallback((index: number, item: T) => {
setList((l) => {
const temp = [...l];
temp[index] = item;
return temp;
});
}, []);
// 删除指定元素
const remove = useCallback((index: number) => {
setList((l) => {
const temp = [...l];
temp.splice(index, 1);
// remove keys if necessary
try {
keyList.current.splice(index, 1);
} catch (e) {
console.error(e);
}
return temp;
});
}, []);
// 移动元素
const move = useCallback((oldIndex: number, newIndex: number) => {
if (oldIndex === newIndex) {
return;
}
setList((l) => {
const newList = [...l];
// 根据 oldIndex 把元素过滤掉
const temp = newList.filter((_, index: number) => index !== oldIndex);
// 根据 newIndex 把过滤掉的元素插入进去
temp.splice(newIndex, 0, newList[oldIndex]);
// move keys if necessary
try {
const keyTemp = keyList.current.filter(
(_, index: number) => index !== oldIndex
);
keyTemp.splice(newIndex, 0, keyList.current[oldIndex]);
keyList.current = keyTemp;
} catch (e) {
console.error(e);
}
return temp;
});
}, []);
// 在列表末尾添加元素
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));
}, []);
// 在列表起始位置添加元素
const unshift = useCallback((item: T) => {
setList((l) => {
setKey(0);
return [item].concat(l);
});
}, []);
// 移除起始位置元素
const shift = useCallback(() => {
// remove keys if necessary
try {
keyList.current = keyList.current.slice(1, keyList.current.length);
} catch (e) {
console.error(e);
}
setList((l) => l.slice(1, l.length));
}, []);
// 校准排序
const sortList = useCallback(
(result: T[]) =>
result
.map((item, index) => ({ key: index, item })) // add index into obj
.sort((a, b) => getIndex(a.key) - getIndex(b.key)) // sort based on the index of table
.filter((item) => !!item.item) // remove undefined(s)
.map((item) => item.item), // retrive the data
[]
);
return {
list,
insert,
merge,
replace,
remove,
getKey,
getIndex,
move,
push,
pop,
unshift,
shift,
sortList,
resetList,
};
};
export default useDynamicList;
useVirtualList
import { type BasicTarget, getTargetElement } from "@/utils/domTarget";
import useMemoizedFn from "../useMemoizedFn";
import useUpdateEffect from "../useUpdateEffect";
import useEventListener from "../useEventListener";
import {
type CSSProperties,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { isNumber } from "@/utils";
import useLatest from "../useLatest";
import useSize from "../useSize";
type ItemHeight<T> = (index: number, data: T) => number;
export interface Options<T> {
containerTarget: BasicTarget;
wrapperTarget: BasicTarget;
itemHeight: number | ItemHeight<T>;
overscan?: number;
}
const useVirtualList = <T = any,>(list: T[], options: Options<T>) => {
const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
const itemHeightRef = useLatest(itemHeight);
// 外部容器尺寸
const size = useSize(containerTarget);
// 标记滚动是否由滚动函数触发
const scrollTriggerByScrollToFunc = useRef(false);
// 当前需要展示的列表内容
const [targetList, setTargetList] = useState<{ index: number; data: T }[]>(
[]
);
// 内部容器样式
const [wrapperStyle, setWrapperStyle] = useState<CSSProperties>({});
// 根据滚动位置计算偏移量
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 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;
};
// 根据索引计算顶部的高度,前面所有列表项的高度总和
const getDistanceTop = (index: number) => {
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
const height = list
.slice(0, index)
.reduce(
(sum, _, i) =>
sum + (itemHeightRef.current as ItemHeight<T>)(i, list[i]),
0
);
return height;
};
// 内部容器的高度
const totalHeight = useMemo(() => {
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
return list.reduce(
(sum, _, index) =>
sum + (itemHeightRef.current as ItemHeight<T>)(index, list[index]),
0
);
}, [list]);
// 计算可见范围内的列表项,并设置内部容器的高度和样式
const calculateRange = () => {
const container = getTargetElement(containerTarget);
if (container) {
const { scrollTop, clientHeight } = container;
// 根据 scrollTop 计算已经 "滚过" 多少项
const offset = getOffset(scrollTop);
// 根据外部容器可视高度和当前的开始索引,计算外部容器能承载的项数
const visibleCount = getVisibleCount(clientHeight, offset);
// 根据 overscan (视区上、下额外展示的 DOM 节点数量) 计算开始索引和结束索引
const start = Math.max(0, offset - overscan);
const end = Math.min(list.length, offset + visibleCount + overscan);
// 根据开始索引计算其距离最开始的距离
const offfsetTop = getDistanceTop(start);
// 设置内部容器的 height 和 marginTop
setWrapperStyle({
height: totalHeight - offfsetTop + "px",
marginTop: offfsetTop + "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]);
// 监听外部容器 scroll 事件
useEventListener(
"scroll",
(e) => {
// 如果滚动是由滚动函数触发,则不需要重新计算
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
calculateRange();
},
{
target: containerTarget,
}
);
// 将 wrapperStyle 应用到内部容器
useUpdateEffect(() => {
const wrapper = getTargetElement(wrapperTarget) as HTMLElement;
if (wrapper) {
Object.keys(wrapperStyle).forEach(
(key) => (wrapper.style[key] = wrapperStyle[key])
);
}
}, [wrapperStyle]);
// 快速滚动到指定 index
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
return [targetList, useMemoizedFn(scrollTo)] as const;
};
export default useVirtualList;
useHistoryTravel
import { isNumber } from "@/utils";
import useMemoizedFn from "../useMemoizedFn";
import { useRef, useState } from "react";
/**
* past - 过去的状态队列
* future - 未来的状态队列
* present - 当前状态
* */
interface IData<T> {
present?: T;
past: T[];
future: T[];
}
// 获取 current 值的下标
const dumpIndex = <T,>(step: number, arr: T[]) => {
// step 大于 0 表示前进,小于 0 表示后退
let index = step > 0 ? step - 1 : arr.length + step;
if (index >= arr.length - 1) {
index = arr.length - 1;
}
if (index < 0) {
index = 0;
}
return index;
};
/**
* 将传入的 targetArr,根据 step,分成当前状态、过去的状态队列、未来的状态队列
* 比如前进,出参为 2 和 [1,2,3,4],得到的结果是 { _current: 2, _before: [1], _after: [3,4] }
* 比如后退,出参为 -1,[1,2,3,4],得到的结果是 { _current: 4, _before: [1, 2, 3], _after: [] }
* */
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),
};
};
const useHistoryTravel = <T,>(initialValue?: T, maxLength: number = 0) => {
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,
past: [],
future: [],
});
};
/**
* 更新
* */
const updateValue = (val: T) => {
// 以前的旧状态队列和 present 合并到新的旧状态队列
const _past = [...past, present];
// 判断 maxLength,超过最大长度后将删除旧状态队列中的第一个记录
const maxLengthNum = isNumber(maxLength) ? maxLength : Number(maxLength);
if (maxLengthNum > 0 && _past.length > maxLengthNum) {
_past.splice(0, 1);
}
setHistory({
present: val,
past: _past,
future: [], // 新状态队列置为空
});
};
/**
* 前进,默认前进一步
* */
const _forward = (step: number = 1) => {
if (future.length === 0) {
return;
}
const { _before, _current, _after } = split(step, future);
setHistory({
// 以前的旧状态队列、present、_before 合并到新的旧状态队列
past: [...past, present, ..._before],
present: _current,
future: _after,
});
};
/**
* 后退,默认后退一步
* */
const _backward = (step: number = -1) => {
if (past.length === 0) {
return;
}
const { _before, _current, _after } = split(step, past);
setHistory({
past: _before,
present: _current,
// 以前的新状态队列、present、_after 合并到新的新状态队列
future: [..._after, present, ...future],
});
};
// 跳到第几步,最终调用 _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),
};
};
export default useHistoryTravel;
useNetwork
import { isObject } from "@/utils";
import { useEffect, useState } from "react";
/**
* since: online 最后改变时间
* online: 网络是否为在线
* rtt: 当前连接下评估的往返时延
* type: 设备使用与所述网络进行通信的连接的类型 bluetooth | cellular | ethernet | none | wifi | wimax | other | unknown
* downlink: 有效带宽估算(单位:兆比特/秒)
* downlinkMax: 最大下行速度(单位:兆比特/秒)
* saveData: 用户代理是否设置了减少数据使用的选项
* effectiveType: 网络连接的类型 slow-2g | 2g | 3g | 4g
* */
export interface NetworkState {
since?: Date;
online?: boolean;
rtt?: number;
type?: string;
downlink?: number;
saveData?: boolean;
downlinkMax?: number;
effectiveType?: string;
}
enum NetworkEventType {
ONLINE = "online",
OFFLINE = "offline",
CHANGE = "change",
}
function getConnection() {
const nav = navigator as any;
if (!isObject(nav)) return null;
return nav.connection || nav.mozConnection || nav.webkitConnection;
}
function getConnectionProperty(): NetworkState {
const c = getConnection();
if (!c) return {};
return {
rtt: c.rtt,
type: c.type,
saveData: c.saveData,
downlink: c.downlink,
downlinkMax: c.downlinkMax,
effectiveType: c.effectiveType,
};
}
const 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(),
}));
};
// 监听网络 online 事件
window.addEventListener(NetworkEventType.ONLINE, onOnline);
// 监听网络 offline 事件
window.addEventListener(NetworkEventType.OFFLINE, onOffline);
// 监听 nav.connection || nav.mozConnection || nav.webkitConnection 的 change 事件
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;
};
export default useNetwork;
useSelections
import { useMemo, useState } from "react";
import type { Key } from "react";
import useMemoizedFn from "../useMemoizedFn";
import { isPlainObject } from "lodash";
import { isFunction, isString } from "@/utils";
export interface Options<T> {
defaultSelected?: T[];
itemKey?: string | ((item: T) => Key);
}
const useSelections = <T,>(items: T[], options?: T[] | Options<T>) => {
let defaultSelected: T[] = [];
let itemKey: Options<T>["itemKey"];
if (Array.isArray(options)) {
defaultSelected = options;
} else if (isPlainObject(options)) {
defaultSelected = options?.defaultSelected ?? defaultSelected;
itemKey = options?.itemKey ?? itemKey;
}
const getKey = (item: T): Key => {
if (isFunction(itemKey)) {
return itemKey(item);
}
if (isString(itemKey) && isPlainObject(item)) {
return item[itemKey];
}
return item as Key;
};
const [selected, setSelected] = useState<T[]>(defaultSelected);
const selectedMap = useMemo(() => {
const keyToItemMap = new Map();
if (!Array.isArray(selected)) {
return keyToItemMap;
}
selected.forEach((item) => {
keyToItemMap.set(getKey(item), item);
});
return keyToItemMap;
}, [selected]);
// 是否被选择
const isSelected = (item: T) => selectedMap.has(getKey(item));
// 选择单个元素
const select = (item: T) => {
selectedMap.set(getKey(item), item);
setSelected(Array.from(selectedMap.values()));
};
// 取消选择单个元素
const unSelect = (item: T) => {
selectedMap.delete(getKey(item));
setSelected(Array.from(selectedMap.values()));
};
// 反选单个元素
const toggle = (item: T) => {
if (isSelected(item)) {
unSelect(item);
} else {
select(item);
}
};
// 选择全部元素
const selectAll = () => {
items.forEach((item) => {
selectedMap.set(getKey(item), item);
});
setSelected(Array.from(selectedMap.values()));
};
// 取消选择全部元素
const unSelectAll = () => {
items.forEach((item) => {
selectedMap.delete(getKey(item));
});
setSelected(Array.from(selectedMap.values()));
};
// 是否一个都没有选择
const noneSelected = useMemo(
() => items.every((item) => !selectedMap.has(getKey(item))),
[items, selectedMap]
);
// 是否全选
const allSelected = useMemo(
() => items.every((item) => selectedMap.has(getKey(item))) && !noneSelected,
[items, selectedMap, noneSelected]
);
// 是否半选
const partiallySelected = useMemo(
() => !noneSelected && !allSelected,
[noneSelected, allSelected]
);
// 反选全部元素
const toggleAll = () => (allSelected ? unSelectAll() : selectAll());
// 清除所有选中元素
const clearAll = () => {
selectedMap.clear();
setSelected([]);
};
return {
selected,
noneSelected,
allSelected,
partiallySelected,
setSelected,
isSelected,
select: useMemoizedFn(select),
unSelect: useMemoizedFn(unSelect),
toggle: useMemoizedFn(toggle),
selectAll: useMemoizedFn(selectAll),
unSelectAll: useMemoizedFn(unSelectAll),
clearAll: useMemoizedFn(clearAll),
toggleAll: useMemoizedFn(toggleAll),
} as const;
};
export default useSelections;
useCountDown
import dayjs from "dayjs";
import { useEffect, useMemo, useState } from "react";
import { isNumber } from "../../../utils";
import useLatest from "@/hooks/useLatest";
export type TDate = dayjs.ConfigType;
export interface Options {
leftTime?: number;
targetDate?: TDate;
interval?: number;
onEnd?: () => void;
}
export interface FormattedRes {
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
// 计算目标时间和当前时间还差多少毫秒
const calcLeft = (target?: TDate) => {
if (!target) {
return 0;
}
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
// 计算剩余时间,目标时间 - 当前时间
const left = dayjs(target).valueOf() - Date.now();
return left < 0 ? 0 : 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 { leftTime, targetDate, interval = 1000, onEnd } = options || {};
const target = useMemo<TDate>(() => {
if ("leftTime" in options) {
return isNumber(leftTime) && leftTime > 0
? Date.now() + leftTime
: undefined;
} else {
return targetDate;
}
}, [leftTime, targetDate]);
const [timeLeft, setTimeLeft] = useState(() => calcLeft(target));
const onEndRef = useLatest(onEnd);
useEffect(() => {
if (!target) {
// for stop
setTimeLeft(0);
return;
}
// 立即执行一次
setTimeLeft(calcLeft(target));
// 通过定时器 setInterval 设置倒计时
const timer = setInterval(() => {
const targetLeft = calcLeft(target);
setTimeLeft(targetLeft);
// 剩余时间为 0,取消定时器,并执行 onEnd 回调
if (targetLeft === 0) {
clearInterval(timer);
onEndRef.current?.();
}
}, interval);
return () => clearInterval(timer);
}, [target, interval]);
const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft]);
return [timeLeft, formattedRes] as const;
};
export default useCountDown;
useCounter
import { isNumber } from "@/utils";
import useMemoizedFn from "../useMemoizedFn";
import { useState } from "react";
export interface Options {
min?: number;
max?: number;
}
export interface Actions {
inc: (delta?: number) => void;
dec: (delta?: number) => void;
set: (value: number | ((c: number) => number)) => void;
reset: () => void;
}
export type ValueParam = number | ((c: number) => number);
/**
* 获取目标数值
* 必须大于等于 min
* 小于等于 max
* */
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;
}
const useCounter = (initialValue: number = 0, options: Options = {}) => {
const { min, max } = options;
const [current, setCurrent] = useState(() => {
return getTargetValue(initialValue, {
min,
max,
});
});
// 设置值,参数可以为 number 或 函数
const setValue = (value: ValueParam) => {
setCurrent((c) => {
const target = isNumber(value) ? value : value(c);
return getTargetValue(target, {
max,
min,
});
});
};
// 加,默认加 1
const inc = (delta: number = 1) => {
setValue((c) => c + delta);
};
// 减,默认减 1
const dec = (delta: number = 1) => {
setValue((c) => c - delta);
};
// 设置 current
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 useCounter;
useTextSelection
import { getTargetElement, type BasicTarget } from "@/utils/domTarget";
import useEffectWithTarget from "@/utils/useEffectWithTarget";
import { useRef, useState } from "react";
interface Rect {
top: number;
left: number;
bottom: number;
right: number;
height: number;
width: number;
}
export interface State extends Rect {
text: string;
}
const initRect: Rect = {
top: NaN,
left: NaN,
bottom: NaN,
right: NaN,
height: NaN,
width: NaN,
};
const initState: State = {
text: "",
...initRect,
};
function getRectFromSelection(selection: Selection | null): Rect {
if (!selection) {
return initRect;
}
// rangeCount readonly 返回该选区所包含的连续范围的数量
if (selection.rangeCount < 1) {
return initRect;
}
// getRangeAt() 方法返回一个包含当前选区内容的区域对象
// index 指定需要被处理的子级编号(从 0 开始),如果该数值被错误的赋予了大于或等于 rangeCount 结果的数字,将会产生错误
const range = selection.getRangeAt(0);
// range.getBoundingClientRect() 返回一个 DOMRect 对象,其绑定了 Range 的整个内容
const { height, width, top, left, right, bottom } =
range.getBoundingClientRect();
return {
height,
width,
top,
left,
right,
bottom,
};
}
const useTextSelection = (target?: BasicTarget<Document | Element>): State => {
const [state, setState] = useState(initState);
const stateRef = useRef(state);
const isInRangeRef = useRef(false);
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();
// toString() 方法返回当前选区的纯文本内容
text = selObj ? selObj.toString() : "";
if (text && isInRangeRef.current) {
rect = getRectFromSelection(selObj);
setState({ ...state, text, ...rect });
}
};
// 鼠标按下时触发回调,重置状态、清除选区
const mousedownHandler = (e) => {
// 如果按下的是右键,则立即返回,这样选中的数据就不会被清空
if (e.button === 2) return;
if (!window.getSelection) return;
// 重置状态
if (stateRef.current.text) {
setState({ ...initState });
}
isInRangeRef.current = false;
// 清除选区
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getSelection
// 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置
const selObj = window.getSelection();
if (!selObj) return;
// https://developer.mozilla.org/zh-CN/docs/Web/API/Selection/removeAllRanges
// Selection.removeAllRanges() 方法会将所有的区域都从选取中移除,只留下 anchorNode 和focusNode 属性并将其设置为 null
// anchorNode readonly 返回该选区起点所在的节点
// focusNode readonly 返回该选区终点所在的节点
selObj.removeAllRanges();
// 检查元素 el 是否包含鼠标事件的目标元素
isInRangeRef.current = el.contains(e.target);
};
// 监听 mouseup 和 mousedown
el.addEventListener("mouseup", mouseupHandler);
document.addEventListener("mousedown", mousedownHandler);
return () => {
el.removeEventListener("mouseup", mouseupHandler);
document.removeEventListener("mousedown", mousedownHandler);
};
},
[],
target
);
return state;
};
export default useTextSelection;
useWebSocket
iimport { useEffect, useRef, useState } from "react";
import useLatest from "../useLatest";
import useUnmount from "../useUnmount";
import useMemoizedFn from "../useMemoizedFn";
/**
* Connecting: 正在连接中
* Open: 已经连接并可以通讯
* Closing: 连接正在关闭
* Closed: 连接已关闭或没有连接成功
* */
export enum ReadyState {
Connecting = 0,
Open = 1,
Closing = 2,
Closed = 3,
}
/**
* reconnectLimit: 重试次数
* reconnectInterval: 重试时间间隔(ms)
* manual: 手动启动连接
* onOpen: 连接成功回调
* onClose: 关闭回调
* onMessage: 收到消息回调
* onError: 错误回调
* protocols: 子协议
* */
export interface Options {
reconnectLimit?: number;
reconnectInterval?: number;
manual?: boolean;
onOpen?: (event: WebSocketEventMap["open"], instance: WebSocket) => void;
onClose?: (event: WebSocketEventMap["close"], instance: WebSocket) => void;
onMessage?: (
message: WebSocketEventMap["message"],
instance: WebSocket
) => void;
onError?: (event: WebSocketEventMap["error"], instance: WebSocket) => void;
protocols?: string | string[];
}
/**
* latestMessage: 最新消息
* sendMessage: 发送消息函数
* disconnect: 手动断开 webSocket 连接
* connect: 手动连接 webSocket,如果当前已有连接,则关闭后重新连接
* readyState: 当前 webSocket 连接状态
* webSocketIns: webSocket 实例
* */
export interface Result {
latestMessage?: WebSocketEventMap["message"];
sendMessage: WebSocket["send"];
disconnect: () => void;
connect: () => void;
readyState: ReadyState;
webSocketIns?: WebSocket;
}
const useWebSocket = (socketUrl: string, options: Options = {}): Result => {
const {
reconnectLimit = 3,
reconnectInterval = 3 * 1000,
manual = false,
onOpen,
onClose,
onMessage,
onError,
protocols,
} = options;
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 [latestMessage, setLatestMessage] =
useState<WebSocketEventMap["message"]>();
const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);
// 重试
const reconnect = () => {
// 没有超过重试次数并且当前 webSocket 实例状态不是 Open
if (
reconnectTimesRef.current < reconnectLimit &&
websocketRef.current?.readyState !== ReadyState.Open
) {
// 如果已经存在重试逻辑,则清除掉计定时器
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
// 重连
reconnectTimerRef.current = setTimeout(() => {
connectWs();
reconnectTimesRef.current++;
}, reconnectInterval);
}
};
// 创建连接
const connectWs = () => {
// 如果已经存在重试逻辑,则清除掉计定时器
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
// 如果当前已有连接,则关闭掉
if (websocketRef.current) {
websocketRef.current?.close();
}
// 创建 webSocket
const ws = new WebSocket(socketUrl, protocols);
setReadyState(ReadyState.Connecting);
// webSocket 错误回调
ws.onerror = (event) => {
if (websocketRef.current !== ws) {
return;
}
// 重试
reconnect();
// 执行错误回调
onErrorRef.current?.(event, ws);
// 修改连接状态
setReadyState(ws.readyState || ReadyState.Closed);
};
// webSocket 连接成功回调
ws.onopen = (event) => {
if (websocketRef.current !== ws) {
return;
}
// 执行连接成功回调
onOpenRef.current?.(event, ws);
// 重置重试次数
reconnectTimesRef.current = 0;
// 修改连接状态
setReadyState(ws.readyState || ReadyState.Open);
};
// webSocket 收到消息回调
ws.onmessage = (message: WebSocketEventMap["message"]) => {
if (websocketRef.current !== ws) {
return;
}
// 执行收到消息回调
onMessageRef.current?.(message, ws);
// 更新最新消息状态
setLatestMessage(message);
};
// webSocket 连接关闭回调
ws.onclose = (event) => {
onCloseRef.current?.(event, ws);
// closed by server
if (websocketRef.current === ws) {
reconnect();
}
// closed by disconnect or closed by server
if (!websocketRef.current || websocketRef.current === ws) {
setReadyState(ws.readyState || ReadyState.Closed);
}
};
// 保存 webSocket 实例
websocketRef.current = ws;
};
// 发送消息函数
const sendMessage: WebSocket["send"] = (message) => {
if (readyState === ReadyState.Open) {
websocketRef.current?.send(message);
} else {
throw new Error("Websocket disconnected");
}
};
// 连接 webSocket,如果当前已有连接,则关闭后重新连接
const connect = () => {
reconnectTimesRef.current = 0;
connectWs();
};
// 手动断开 webSocket 连接
const disconnect = () => {
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
reconnectTimesRef.current = reconnectLimit;
websocketRef.current?.close();
websocketRef.current = undefined;
};
useEffect(() => {
// 是否手动启动连接
if (!manual && socketUrl) {
connect();
}
}, [socketUrl, manual]);
// 组件销毁
useUnmount(() => {
disconnect();
});
return {
latestMessage,
sendMessage: useMemoizedFn(sendMessage),
connect: useMemoizedFn(connect),
disconnect: useMemoizedFn(disconnect),
readyState,
webSocketIns: websocketRef.current,
};
};
export default useWebSocket;