useSync
简介
一个仿 react-query 的数据请求 custom-hook。
根据数据的请求情况分为了请求中、成功、失败等几种状态,
可以直接拿里面的状态去做一些页面上加载中、给出错误信息等的优化,
同时,不用在组件中再定义额外的状态,达到了精研代码、让代码更加优雅的目的。
在没有react-query的情况下,是很好的数据请求实践。
主要变量和api
isIdle 首次加载
isLoading 请求中
isError 错误信息
isSuccess 成功的状态
run 主要的处理函数
setData 保存数据的方法
setError 保存错误
...state, 暴露所有状态 (error、state 、data)
retry 再次请求
const useProjects = (param?: Partial<Project>) => {
const client = useHttp();
const { run, ...result } = useSync<Project[]>();
const fetchProjects = useCallback(
() => client("projects", { data: cleanObject(param || {}) }),
[client, param]
);
useEffect(() => {
run(fetchProjects(), { retry: fetchProjects });
}, [fetchProjects, param, run]);
return result;
};
const { isLoading, data: list, error, retry } = useProjects(debouncedParam);
<List
refresh={retry}
loading={isLoading}
users={users || []}
dataSource={list || []}
/>
hook解读
run 函数是主要的执行函数,
它负责将传入的promise进行处理,
进入即修改状态为loading。
正常情况下使用 setData 保存数据,同时修改状态为success。
使用 useMountRef hook 判断当前的组件状态,
若当前为卸载状态,就不保存数据。
出现错误的情况下则 使用 setError 保存错误信息,修改状态为error。
最后, 暴露几种状态,run、retry函数 和 state。
useState实现
import { useCallback, useState } from "react";
import { useMountRef } from "./useMountRef";
interface State<D> {
error: Error | null
data: D | null
stat: 'idle' | 'loading' | 'error' | 'succcess'
}
const defaultInitialState : State<null> = {
error: null,
data: null,
stat: "idle"
}
const defaultConfig = {
throwOnError: false
};
const useSync = <D>(initialState?: State<D>, initialConfig?: typeof defaultConfig) => {
const config = {...defaultConfig, initialConfig}
const [state, setState] = useState<State<D>>({
...defaultInitialState,
...initialState,
});
const mountedRef = useMountRef();
const [retry, setRetry] = useState(() => () => {});
const setData = useCallback((data: D) => {
setState({
data,
stat: 'succcess',
error: null
});
},[]);
const setError = useCallback((error: Error) => {
setState({
error,
stat: 'error',
data: null,
});
},[]);
const run = useCallback((promise: Promise<D>, runConfig?: {retry: () => Promise<D>}) => {
if(!promise || !promise.then) {
throw new Error('请传入promise类型数据');
}
setRetry(() => () => {
if (runConfig?.retry) {
run(runConfig.retry() , runConfig);
}
});
setState(prevState => {
return {
...prevState,
stat: 'loading',
}
})
return promise
.then(data => {
if (mountedRef.current)
setData(data);
return data;
})
.catch(error => {
setError(error);
if (config.throwOnError) return Promise.reject(error);
return error;
})
},[config.throwOnError, mountedRef, setData, setError]);
return {
isIdle: state.stat === 'idle',
isLoading: state.stat === 'loading',
isError: state.stat === 'error',
isSuccess: state.stat === 'succcess',
run,
setData,
setError,
...state,
retry,
};
};
export default useSync;
useReducer实现(另一个版本)
import { useCallback, useReducer, useState } from "react";
import { useMountRef } from "./useMountRef";
interface State<D> {
error: Error | null
data: D | null
stat: 'idle' | 'loading' | 'error' | 'succcess'
}
const defaultInitialState : State<null> = {
error: null,
data: null,
stat: "idle"
}
const defaultConfig = {
throwOnError: false
};
const useSafeDispatch = <T>(dispatch: (...args: T[]) => void) => {
const mountedRef = useMountRef();
return useCallback((...args: T[]) => (mountedRef.current ? dispatch(...args): void 0), [dispatch, mountedRef]);
};
const useSync = <D>(initialState?: State<D>, initialConfig?: typeof defaultConfig) => {
const config = {...defaultConfig, initialConfig}
const [state, dispatch] = useReducer((state: State<D>, actions: Partial<State<D>>) => ({...state, ...actions}),{
...defaultInitialState,
...initialState,
});
const safeDispatch = useSafeDispatch(dispatch);
const [retry, setRetry] = useState(() => () => {});
const setData = useCallback((data: D) => {
safeDispatch({
data,
stat: 'succcess',
error: null
});
},[safeDispatch]);
const setError = useCallback((error: Error) => {
safeDispatch({
error,
stat: 'error',
data: null
});
},[safeDispatch]);
const run = useCallback((promise: Promise<D>, runConfig?: {retry: () => Promise<D>}) => {
if(!promise || !promise.then) {
throw new Error('请传入promise类型数据');
}
setRetry(() => () => {
if (runConfig?.retry) {
run(runConfig.retry() , runConfig);
}
});
safeDispatch({stat: 'loading'})
return promise
.then(data => {
setData(data);
return data;
})
.catch(error => {
setError(error);
if (config.throwOnError) return Promise.reject(error);
return error;
})
},[config.throwOnError, safeDispatch, setData, setError]);
return {
isIdle: state.stat === 'idle',
isLoading: state.stat === 'loading',
isError: state.stat === 'error',
isSuccess: state.stat === 'succcess',
run,
setData,
setError,
...state,
retry,
};
};
export default useSync;
useMountRef
返回一个布尔值标识组件当前的状态,true 为挂载, false为卸载。
import { useEffect, useRef } from "react";
export const useMountRef = () => {
const mountedRef = useRef(false);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
}
});
return mountedRef;
};
useDocumentTitle
一个改变标签页文字标题的hook
keepOnUnmount 参数决定了 是否 回退时回退到之前的标题
useDocumentTitle('请登录注册以继续', false);
import { useEffect, useRef } from "react";
const useDocumentTitle = (title: string, keepOnUnmount: boolean = true) => {
const oldTitle = useRef(document.title).current;
useEffect(() => {
document.title = title;
}, [title]);
useEffect(() => {
return () => {
if(!keepOnUnmount) {
document.title = oldTitle;
}
}
}, [keepOnUnmount, oldTitle]);
}
export default useDocumentTitle;
useQueryParam
export const useSetUrlSearchParam = () => {
const [searchParams, setSearchParams] = useSearchParams();
return (params: {[key in string]: unknown}) => {
const o = cleanObject({
...Object.fromEntries(searchParams),
...params
}) as URLSearchParamsInit;
return setSearchParams(o);
};
};
export const useQueryParam = <K extends string>(keys: K[]) => {
const [searchParams] = useSearchParams();
const setSearchParams = useSetUrlSearchParam();
return [
useMemo(
() => keys.reduce((prev, key) => {
return {...prev, [key]: searchParams.get(key) || ''}
}, {} as {[key in K]: string})
, [keys,searchParams]),
(params: Partial<{[key in K]: unknown}>) => {
return setSearchParams(params);
}
] as const
};
export const useProjectSearchParam = () => {
const [param, setParam] = useQueryParam(['name', 'personId']);
return [
useMemo(
() => ({...param, personId: Number(param.personId) || undefined}),
[param]
),
setParam
] as const
};
const [param, setParam] = useProjectSearchParam();
const debouncedParam = useDebounce(param, 500);
const { isLoading, data: list } = useProjects(debouncedParam);
<Form.Item>
<Input
placeholder="项目名"
type="text"
value={param.name}
onChange={(event) => {
setParam({
...param,
name: event.target.value,
});
}}
/>
export const useProjectModal = () => {
const [{projectCreate}, setProjectCreate] = useQueryParam(['projectCreate']);
const [{editingProjectId}, setEditingProjectId] = useQueryParam(['editingProjectId']);
const setUrlParams = useSetUrlSearchParam();
const {data: editingProject, isLoading} = useProject(Number(editingProjectId));
const open = () => {
setProjectCreate({projectCreate: true});
};
const close = () => {
setUrlParams({projectCreate: '', editingProjectId: ''});
};
const startEdit = (id: number) => {
setEditingProjectId({editingProjectId: id})
};
return {
projectModalOpen: projectCreate === 'true' || Boolean(editingProjectId),
open,
close,
startEdit,
editingProject,
isLoading,
};
};
useArray
export const useArray = <T> (initialArray: T[]) => {
const [value, setValue] = useState(initialArray)
return {
value,
setValue,
add: (item: T) => setValue([...value, item]),
clear: () => setValue([]),
removeIndex: (index: number) => {
const copy = [...value]
copy.splice(index, 1)
setValue(copy)
}
}
}