Skip to content

Instantly share code, notes, and snippets.

@librz
Last active March 25, 2024 08:31
Show Gist options
  • Save librz/67839609a80dfcc81d9767c667c77882 to your computer and use it in GitHub Desktop.
Save librz/67839609a80dfcc81d9767c667c77882 to your computer and use it in GitHub Desktop.
解决 react-router-dom v5 没有 useSearchParams 的问题
/**
* 以下代码来自 react-router-dom v6,用于解决 react-router-dom v5 没有 useSearchParams 的问题
* see: https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/index.tsx#L1432
* 在原代码的基础上做的改动:
* 1. 原代码使用了 useNavigate,但 react-router v5 还没有这个 hook,使用 useHistory 代替(https://reactrouter.com/en/main/upgrading/v5#use-usenavigate-instead-of-usehistory)
* 2. 在返回的数组中提供 updateSearchParams 以在保留其他参数的情况下设置/更新指定的参数
*/
import React, { useCallback } from 'react';
import { useLocation, useHistory } from 'react-router';
type ParamKeyValuePair = [string, string];
type URLSearchParamsInit =
| string
| ParamKeyValuePair[]
| Record<string, string | string[]>
| URLSearchParams;
type NavigateOptions = {
replace?: boolean;
state?: unknown;
};
type SetURLSearchParams = (
nextInit?: URLSearchParamsInit | ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOptions?: NavigateOptions
) => void;
type UpdateURLSearchParams = (
params: Record<string, string | null | undefined>,
navigateOptions?: NavigateOptions
) => void;
/**
* Creates a URLSearchParams object using the given initializer.
*
* This is identical to `new URLSearchParams(init)` except it also
* supports arrays as values in the object form of the initializer
* instead of just strings. This is convenient when you need multiple
* values for a given key, but don't want to use an array initializer.
*
* For example, instead of:
*
* let searchParams = new URLSearchParams([
* ['sort', 'name'],
* ['sort', 'price']
* ]);
*
* you can do:
*
* let searchParams = createSearchParams({
* sort: ['name', 'price']
* });
*/
function createSearchParams(init: URLSearchParamsInit = ''): URLSearchParams {
return new URLSearchParams(
typeof init === 'string' || Array.isArray(init) || init instanceof URLSearchParams
? init
: Object.keys(init).reduce((memo, key) => {
let value = init[key];
return memo.concat(Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]);
}, [] as ParamKeyValuePair[])
);
}
export function getSearchParamsForLocation(
locationSearch: string,
defaultSearchParams: URLSearchParams | null
) {
let searchParams = createSearchParams(locationSearch);
if (defaultSearchParams) {
// Use `defaultSearchParams.forEach(...)` here instead of iterating of
// `defaultSearchParams.keys()` to work-around a bug in Firefox related to
// web extensions. Relevant Bugzilla tickets:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1414602
// https://bugzilla.mozilla.org/show_bug.cgi?id=1023984
defaultSearchParams.forEach((_, key) => {
if (!searchParams.has(key)) {
defaultSearchParams.getAll(key).forEach((value) => {
searchParams.append(key, value);
});
}
});
}
return searchParams;
}
/**
* A convenient wrapper for reading and writing search parameters via the
* URLSearchParams interface.
*/
export function useSearchParams(
defaultInit?: URLSearchParamsInit
): [URLSearchParams, SetURLSearchParams, UpdateURLSearchParams] {
let defaultSearchParamsRef = React.useRef(createSearchParams(defaultInit));
let hasSetSearchParamsRef = React.useRef(false);
let location = useLocation();
let searchParams = React.useMemo(
() =>
// Only merge in the defaults if we haven't yet called setSearchParams.
// Once we call that we want those to take precedence, otherwise you can't
// remove a param with setSearchParams({}) if it has an initial value
getSearchParamsForLocation(
location.search,
hasSetSearchParamsRef.current ? null : defaultSearchParamsRef.current
),
[location.search]
);
let history = useHistory();
let setSearchParams = React.useCallback(
(
nextInit?: URLSearchParamsInit | ((prev: URLSearchParams) => URLSearchParamsInit),
navigateOptions?: NavigateOptions
) => {
const newSearchParams = createSearchParams(
typeof nextInit === 'function' ? nextInit(searchParams) : nextInit
);
hasSetSearchParamsRef.current = true;
if (navigateOptions?.replace) {
history.replace(`?${newSearchParams}`, navigateOptions.state);
} else {
history.push(`?${newSearchParams}`, navigateOptions?.state);
}
},
[history, searchParams]
);
// perserve other keys when seting search params
const updateSearchParams = useCallback(
(params: Record<string, string | null | undefined>, navigateOptions?: NavigateOptions) => {
setSearchParams((prev) => {
const newSearchParams = new URLSearchParams(prev);
Object.entries(params).forEach(([key, value]) => {
if (!value) {
newSearchParams.delete(key);
} else {
newSearchParams.set(key, value);
}
});
return newSearchParams;
}, navigateOptions);
},
[setSearchParams]
);
return [searchParams, setSearchParams, updateSearchParams];
}
import { useCallback, useMemo } from 'react';
import { useSearchParams } from './useSearchParams';
import { isNil, omitBy } from 'lodash';
export function useStructQuery<T = any>(queryKey = 'query') {
// eslint-disable-next-line
const [searchParams, _, updateSearchParams] = useSearchParams();
const queryString = searchParams.get(queryKey);
const queryStruct = useMemo(() => {
if (!queryString) return null;
let result: Partial<{ [key in keyof T]: string }> = {};
try {
const parsed = JSON.parse(queryString);
result = omitBy(parsed, isNil);
} catch (e) {
console.error(`JSON.parse failed for query string: ${queryString}`, e);
result = {};
}
return result;
}, [queryString]);
const setQueryStruct = useCallback(
(newStruct: Partial<T> | null) => {
const struct = omitBy(newStruct, isNil);
if (!struct) {
updateSearchParams({ [queryKey]: null });
return;
}
if (Object.keys(struct).length === 0) {
updateSearchParams({ [queryKey]: null });
return;
}
// stringify
let newQueryString: string | null = null;
try {
newQueryString = JSON.stringify(struct);
} catch (e) {
console.error(`JSON.stringify failed for query struct:`, struct, e);
newQueryString = null;
}
updateSearchParams({ [queryKey]: newQueryString });
},
[queryKey, updateSearchParams]
);
return [queryStruct, setQueryStruct] as const;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment