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;
export default function useUrlSearchState(
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();
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) => {
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;
export default function useUrlSearchState(
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) => {
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(
obj: T | null,
...args: Parameters | [string, Function | null, ...any]
): void {
if (obj && obj.addEventListener) {
obj.addEventListener(...(args as Parameters));
}
}
export function off(
obj: T | null,
...args: Parameters | [string, Function | null, ...any]
): void {
if (obj && obj.removeEventListener) {
obj.removeEventListener(...(args as Parameters));
}
}
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;