react-custom-hook-two


useSync

简介


  一个仿 react-query 的数据请求 custom-hook。
  
  根据数据的请求情况分为了请求中、成功、失败等几种状态,
  
  可以直接拿里面的状态去做一些页面上加载中、给出错误信息等的优化,
  
  同时,不用在组件中再定义额外的状态,达到了精研代码、让代码更加优雅的目的。
  
  在没有react-query的情况下,是很好的数据请求实践。

主要变量和api


  isIdle 首次加载
  isLoading 请求中
  isError 错误信息
  isSuccess 成功的状态
  run 主要的处理函数
  setData 保存数据的方法
  setError 保存错误
  ...state, 暴露所有状态 (error、state 、data)
  retry 再次请求
// use

const useProjects = (param?: Partial<Project>) => {
  const client = useHttp();
  const { run, ...result } = useSync<Project[]>();

  const fetchProjects = useCallback(
    () => client("projects", { data: cleanObject(param || {}) }),
    [client, param]
  );

  useEffect(() => {
    // retry 的使用方法 , 传入第二个为对象的参数
    run(fetchProjects(), { retry: fetchProjects });
  }, [fetchProjects, param, run]);

  return result;
};

// project index.tsx
const { isLoading, data: list, error, retry } = useProjects(debouncedParam);

// 传给一个组件

/**
* 注意: 这里如果要使用 retry,
*
* 必须使用 useProjects(...result) 暴露出来的, 
*
* 如果使用在 project index.tsx 文件 再次调用 useSync 得来的 retry,
*
* 是没用作用的,不会调用接口!
*
* 因为 useProjects 中定义了 retry 函数,
* 
* 而 再次调用的 useSync没有定义 retry 函数,同时也没有其他的状态和其他的东西!
*
* 这是一个比较容易犯的错误。
*/

<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,
        });
    },[]);

    // run 用来触发异步请求
    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);
            }
            // 不能用这种方式
            // if (promise) {
            //     run(promise);
            // }
        });
        setState(prevState => { // 这样state就不会引入到依赖项 引发无限渲染了
            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实现(另一个版本)

// 本质是一样的,但 useReducer 更适合管理这种多个状态的情况

// 外部其他hook调用、组件使用与state-hook完全一致

import { useCallback, useReducer, useState } from "react";
import { useMountRef } from "./useMountRef";

// useSycn useReducer 实现
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]);

    // run 用来触发异步请求
    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);
            }
            // 不能用这种方式
            // if (promise) {
            //     run(promise);
            // }
        });
        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 参数决定了 是否 回退时回退到之前的标题
 // use

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

// 前置

// 修改hook

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);
    };
};
// 返回一个数组 第一个元素为 {}, 里面是传入的 key 与她的值 的集合

// 第二个元素为一个函数 ,她负责修改 key 值集合 的值 或者是清空('' || undefined)

export const useQueryParam = <K extends string>(keys: K[]) => {
    const [searchParams] = useSearchParams();
    const setSearchParams = useSetUrlSearchParam();

    return [
        // useMemo (解决新创建对象 和 usedebounce) 引发的无限循环问题
        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 // as const 将数组类型变为一个 tuple 的元祖类型,类型上更加准确,而不是一前一后两个元素类型一致的情况!
};
// use onetype

export const  useProjectSearchParam = () => {
    const [param, setParam] = useQueryParam(['name', 'personId']); // 传入 key[]

    return [
        useMemo(
            () => ({...param, personId: Number(param.personId) || undefined}), 
            [param]
        ),
        setParam
    ] as const // as const 解决了使用时 变量和函数类型报错的问题
};

// index.tsx 传给  searchPanel

const [param, setParam] = useProjectSearchParam();

/**
* 问题: 这样操作 是怎么调用接口的呢?
*
* 答:useProjects 使用了 react-query 
*
* param 作为 useQuery 的第二个参数,即依赖项, 因此param 发生变化时 useQuery 会再次执行调用接口
*
*/

const debouncedParam = useDebounce(param, 500); // 对值进行debounce,只取最后结果

const { isLoading, data: list } = useProjects(debouncedParam);

// searchPanel.tsx

<Form.Item>
  <Input
    placeholder="项目名"
    type="text"
    value={param.name}
    onChange={(event) => {
      setParam({
        ...param,
        name: event.target.value,
      });
    }}
  />
// use twotype

// 直接结构 传入的 key[] ,key 即是键

/**
* 一般我们modal的显示隐藏会使用 useState保存、
* 
* 或者是使用 redux。
*
* 而除此之外,我们还可以使用url参数来管理,
* 
* 这里需要借助 react-router-dom 的 useSearchParams,
*
* 这样可以控制modal的打开关闭,
* 
* 也可以在编辑时,通过url保存一个id,再通过这个id得到具体的修改项,
* 
* 把这一项直接传入modal渲染,
*
* 这样,modal.tsx 就只扮演view的角色,
* 
* 不用维护太多逻辑和状态,
* 
* 也更加符合逻辑与视图抽离的思想。
*/

/**
*
* projectModalOpen 是否为打开状态, 存在修改的 editingProjectId 或者  projectCreate 都为打开
* 
* open 打开的函数
*
* close 关闭的函数 ,将打开、关闭的 的值清空
*
* editingProject 根据点击修改 得到的id,请求接口得到的具体修改的那一项
*
* 具体修改的那一项 的 isLoading 数据请求 loading 态
*/

export const useProjectModal = () => {
    const [{projectCreate}, setProjectCreate] = useQueryParam(['projectCreate']);
    const [{editingProjectId}, setEditingProjectId] = useQueryParam(['editingProjectId']);

    const setUrlParams = useSetUrlSearchParam();
    // 修改时的 projects-list 的一项
    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 {// 这里 useQueryParam 的第二个参数 (函数)会把 参数值 变 成一个字符串, 所以 为 'true'
        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)
    }
  }
}

文章作者: KarlFranz
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 reprint policy. If reproduced, please indicate source KarlFranz !
评论
  目录