美文网首页
React 自定义hook 通过 url search 来管理

React 自定义hook 通过 url search 来管理

作者: 学生黄哲 | 来源:发表于2022-01-11 11:25 被阅读0次
    1、使用 react-router

    useUrlSearchState 使用 react-router 6.x 版本来管理路由 如需使用5.x使用 useHistory 替换 useNavigate

    /* eslint-disable react-hooks/exhaustive-deps */
    import { parse, stringify } from 'query-string';
    import type { ParseOptions, StringifyOptions } from 'query-string';
    import { useMemo, useRef, useCallback, SetStateAction, useState, useEffect } from 'react';
    import { useLocation, useNavigate } from 'react-router';
    
    function getDefaultSearch(searchKeys?: string[]) {
      if (!searchKeys) return;
    
      const obj: any = {};
      searchKeys.forEach((i) => {
        obj[i] = undefined;
      });
      return obj;
    }
    function getSearchObj(searchKeys?: string[], searchObj?: any) {
      if (!searchKeys) return searchObj;
    
      const obj: any = {};
      searchKeys.forEach((i) => {
        if (searchObj[i]) obj[i] = searchObj[i];
      });
      return obj;
    }
    
    export interface Options {
      navigateMode?: 'push' | 'replace';
      parseOptions?: ParseOptions;
      stringifyOptions?: StringifyOptions;
      searchKeys?: string[];
    }
    
    const baseParseConfig: ParseOptions = {
      parseNumbers: false,
      parseBooleans: false,
      arrayFormat: 'index',
    };
    
    const baseStringifyConfig: StringifyOptions = {
      skipNull: false,
      skipEmptyString: false,
      arrayFormat: 'index',
    };
    
    type UrlState = Record<string, any>;
    
    export default function useUrlSearchState<S extends UrlState = UrlState>(
      initialState?: S | (() => S),
      options?: Options,
    ) {
      type State = Partial<{ [key in keyof S]: any }>;
      const { navigateMode = 'push', parseOptions, stringifyOptions, searchKeys } = options || {};
    
      const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
      const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };
    
      const navigate = useNavigate();
      const [, update] = useState({});
      const { search, hash } = useLocation();
      const hashRef = useRef<string>();
      useEffect(() => {
        hashRef.current = hash;
      });
      const initialStateRef = useRef(typeof initialState === 'function' ? (initialState as () => S)() : initialState || {});
    
      const queryFromUrl = useMemo(() => {
        return parse(search, mergedParseOptions);
      }, [search]);
      const defaultSearch = useMemo(() => getDefaultSearch(searchKeys), [searchKeys]);
    
      const targetQuery: State = useMemo(
        () =>
          getSearchObj(searchKeys, {
            ...initialStateRef.current,
            ...queryFromUrl,
          }),
        [queryFromUrl],
      );
    
      const setSearch = useCallback(
        (search: string) => {
          navigate({ hash: hashRef.current, search }, { replace: navigateMode === 'replace' });
        },
        [navigate],
      );
    
      const setState = useCallback(
        (s: SetStateAction<State>) => {
          const newQuery = typeof s === 'function' ? s(targetQuery) : s;
          setSearch(stringify({ ...queryFromUrl, ...(defaultSearch ?? {}), ...newQuery }, mergedStringifyOptions) || '?');
          update({});
        },
        [setSearch, queryFromUrl, targetQuery, defaultSearch],
      );
    
      const clear = useCallback(() => {
        !defaultSearch ? setSearch('') : setState(defaultSearch);
      }, [defaultSearch, setState]);
    
      return [targetQuery, setState, clear] as const;
    }
    
    
    
    2、如果没有使用react-router来管理路由

    注:使用此版本useUrlSearchState不要和hash一块使用,如果需要使用hash来管理state可以使用 react-use useHash

    useUrlSearchState.ts

    /* eslint-disable react-hooks/exhaustive-deps */
    import { parse, stringify } from 'query-string';
    import type { ParseOptions, StringifyOptions } from 'query-string';
    import { useMemo, useRef, useCallback, SetStateAction, useState } from 'react';
    import useLocation from './useLocation';
    
    function getDefaultSearch(searchKeys?: string[]) {
      if (!searchKeys) return;
    
      const obj: any = {};
      searchKeys.forEach((i) => {
        obj[i] = undefined;
      });
      return obj;
    }
    function getSearchObj(searchKeys?: string[], searchObj?: any) {
      if (!searchKeys) return searchObj;
    
      const obj: any = {};
      searchKeys.forEach((i) => {
        if (searchObj[i]) obj[i] = searchObj[i];
      });
      return obj;
    }
    
    export interface Options {
      navigateMode?: 'push' | 'replace'; //切换 history 的方式
      parseOptions?: ParseOptions; //query-string ParseOptions
      stringifyOptions?: StringifyOptions; //query-string StringifyOptions
      searchKeys?: string[]; //需要管理的keys
    }
    
    const baseParseConfig: ParseOptions = {
      parseNumbers: false,
      parseBooleans: false,
      arrayFormat: 'index',
    };
    
    const baseStringifyConfig: StringifyOptions = {
      skipNull: false,
      skipEmptyString: false,
      arrayFormat: 'index',
    };
    
    type UrlState = Record<string, any>;
    
    export default function useUrlSearchState<S extends UrlState = UrlState>(
      initialState?: S | (() => S),
      options?: Options,
    ) {
      type State = Partial<{ [key in keyof S]: any }>;
      const { navigateMode = 'push', parseOptions, stringifyOptions, searchKeys } = options || {};
    
      const mergedParseOptions = { ...baseParseConfig, ...parseOptions };
      const mergedStringifyOptions = { ...baseStringifyConfig, ...stringifyOptions };
    
      const [, update] = useState({});
      const { search } = useLocation();
    
      const initialStateRef = useRef(typeof initialState === 'function' ? (initialState as () => S)() : initialState || {});
    
      const queryFromUrl = useMemo(() => {
        return parse(search || '', mergedParseOptions);
      }, [search]);
      const defaultSearch = useMemo(() => getDefaultSearch(searchKeys), [searchKeys]);
      const targetQuery: State = useMemo(
        () =>
          getSearchObj(searchKeys, {
            ...initialStateRef.current,
            ...queryFromUrl,
          }),
        [queryFromUrl, searchKeys],
      );
      const setSearch = useCallback((search: string) => {
        history[navigateMode === 'replace' ? 'replaceState' : 'pushState'](
          null,
          '',
          search ? '?' + search : location.origin,
        );
      }, []);
    
      const setState = useCallback(
        (s: SetStateAction<State>) => {
          const newQuery = typeof s === 'function' ? s(targetQuery) : s;
          setSearch(stringify({ ...queryFromUrl, ...(defaultSearch ?? {}), ...newQuery }, mergedStringifyOptions) || '?');
          update({});
        },
        [setSearch, queryFromUrl, targetQuery, defaultSearch],
      );
    
      const clear = useCallback(() => {
        !defaultSearch ? setSearch('') : setState(defaultSearch);
      }, [defaultSearch, setSearch, setState]);
    
      return [targetQuery, setState, clear] as const;
    }
    

    useLocation.ts fork 自 react-use useLocation

    import { useEffect, useState } from 'react';
    
    export function on<T extends Window | Document | HTMLElement | EventTarget>(
      obj: T | null,
      ...args: Parameters<T['addEventListener']> | [string, Function | null, ...any]
    ): void {
      if (obj && obj.addEventListener) {
        obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>));
      }
    }
    
    export function off<T extends Window | Document | HTMLElement | EventTarget>(
      obj: T | null,
      ...args: Parameters<T['removeEventListener']> | [string, Function | null, ...any]
    ): void {
      if (obj && obj.removeEventListener) {
        obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>));
      }
    }
    
    export const isBrowser = typeof window !== 'undefined';
    
    const patchHistoryMethod = (method: 'pushState' | 'replaceState') => {
      const history = window.history;
      const original = history[method];
    
      history[method] = function (state) {
        // eslint-disable-next-line prefer-rest-params
        const result = original.apply(this, arguments as any);
        const event = new Event(method.toLowerCase());
    
        (event as any).state = state;
    
        window.dispatchEvent(event);
    
        return result;
      };
    };
    
    if (isBrowser) {
      patchHistoryMethod('pushState');
      patchHistoryMethod('replaceState');
    }
    
    export interface LocationSensorState {
      trigger: string;
      state?: any;
      length?: number;
      hash?: string;
      host?: string;
      hostname?: string;
      href?: string;
      origin?: string;
      pathname?: string;
      port?: string;
      protocol?: string;
      search?: string;
    }
    
    const useLocationServer = (): LocationSensorState => ({
      trigger: 'load',
      length: 1,
    });
    
    const buildState = (trigger: string) => {
      const { state, length } = window.history;
    
      const { hash, host, hostname, href, origin, pathname, port, protocol, search } = window.location;
    
      return {
        trigger,
        state,
        length,
        hash,
        host,
        hostname,
        href,
        origin,
        pathname,
        port,
        protocol,
        search,
      };
    };
    
    const useLocationBrowser = (): LocationSensorState => {
      const [state, setState] = useState(buildState('load'));
    
      useEffect(() => {
        const onPopstate = () => setState(buildState('popstate'));
        const onPushstate = () => setState(buildState('pushstate'));
        const onReplacestate = () => setState(buildState('replacestate'));
    
        on(window, 'popstate', onPopstate);
        on(window, 'pushstate', onPushstate);
        on(window, 'replacestate', onReplacestate);
    
        return () => {
          off(window, 'popstate', onPopstate);
          off(window, 'pushstate', onPushstate);
          off(window, 'replacestate', onReplacestate);
        };
      }, []);
    
      return state;
    };
    
    const hasEventConstructor = typeof Event === 'function';
    
    export default isBrowser && hasEventConstructor ? useLocationBrowser : useLocationServer;
    

    相关文章

      网友评论

          本文标题:React 自定义hook 通过 url search 来管理

          本文链接:https://www.haomeiwen.com/subject/xsebcrtx.html