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)
    }
  }
}