Last active
November 5, 2023 12:27
-
-
Save simonrelet/d30c389c837d78b11aae71121b548c94 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useSearchParams } from "@remix-run/react"; | |
import isEqual from "lodash.isequal"; | |
import { useCallback, useMemo } from "react"; | |
import type { NavigateOptions } from "react-router"; | |
export function useParsedSearchParams< | |
TOutput extends object, | |
TInput extends object, | |
>(delegate: URLSearchParamsDelegate<TOutput, TInput>) { | |
const [searchParams, setSearchParams] = useSearchParams(); | |
const parsedSearchParams = useMemo( | |
() => delegate.parse(searchParams), | |
[searchParams, delegate], | |
); | |
const setParsedSearchParams = useCallback( | |
(data: TInput, navigateOptions?: NavigateOptions) => { | |
setSearchParams((searchParams) => { | |
delegate.set(searchParams, data); | |
return searchParams; | |
}, navigateOptions); | |
}, | |
[setSearchParams, delegate], | |
); | |
return [parsedSearchParams, setParsedSearchParams] as const; | |
} | |
export class URLSearchParamsDelegate< | |
TOutput extends object, | |
TInput extends object, | |
> | |
implements Parser<TOutput>, Formatter<TInput> | |
{ | |
protected parser: URLSearchParamsParser<TOutput>; | |
protected formatter: URLSearchParamsFormatter<TInput>; | |
constructor({ | |
parser, | |
formatter, | |
}: { | |
parser: URLSearchParamsParser<TOutput>; | |
formatter: URLSearchParamsFormatter<TInput>; | |
}) { | |
this.parser = parser; | |
this.formatter = formatter; | |
} | |
parse(searchParams: URLSearchParams) { | |
return this.parser.parse(searchParams); | |
} | |
isEmpty(searchParams: URLSearchParams) { | |
return this.parser.isEmpty(searchParams); | |
} | |
areEqual(a: URLSearchParams, b: URLSearchParams) { | |
return this.parser.areEqual(a, b); | |
} | |
format(data: TInput) { | |
return this.formatter.format(data); | |
} | |
set(searchParams: URLSearchParams, data: TInput) { | |
this.formatter.set(searchParams, data); | |
} | |
} | |
export namespace URLSearchParamsDelegate { | |
export type Infer<TDelegate extends URLSearchParamsDelegate<any, any>> = | |
TDelegate extends URLSearchParamsDelegate<infer TOutput, any> | |
? TOutput | |
: never; | |
} | |
export class URLSearchParamsParser<TOutput extends object> | |
implements Parser<TOutput> | |
{ | |
// It's not useless because it defines `parseFunction` as protected instance | |
// attribute. | |
// eslint-disable-next-line no-useless-constructor | |
constructor(protected parseFunction: (data: object) => TOutput) {} | |
parse(searchParams: URLSearchParams) { | |
return this.parseFunction(toObject(searchParams)); | |
} | |
isEmpty(searchParams: URLSearchParams) { | |
return this.areEqual(searchParams, new URLSearchParams()); | |
} | |
areEqual(a: URLSearchParams, b: URLSearchParams) { | |
return isEqual(this.parse(a), this.parse(b)); | |
} | |
} | |
interface Parser<TOutput extends object> { | |
parse(searchParams: URLSearchParams): TOutput; | |
isEmpty(searchParams: URLSearchParams): boolean; | |
areEqual(a: URLSearchParams, b: URLSearchParams): boolean; | |
} | |
function toObject(searchParams: URLSearchParams): object { | |
const valuesByKey = new Map<string, unknown[]>(); | |
for (const [key, value] of searchParams) { | |
const currentValue = valuesByKey.get(key); | |
if (currentValue == null) { | |
valuesByKey.set(key, [value]); | |
} else { | |
currentValue.push(value); | |
} | |
} | |
return Object.fromEntries( | |
Array.from(valuesByKey.entries()).map(([key, value]) => [ | |
key, | |
value.length === 1 ? value[0] : value, | |
]), | |
); | |
} | |
export class URLSearchParamsFormatter<TInput extends object> | |
implements Formatter<TInput> | |
{ | |
// It's not useless because it defines `preprocess` as protected instance | |
// attribute. | |
// eslint-disable-next-line no-useless-constructor | |
constructor( | |
protected preprocess: (data: TInput) => object = (data) => data, | |
) {} | |
format(data: TInput) { | |
const searchParams = new URLSearchParams(); | |
this.set(searchParams, data); | |
return searchParams.toString(); | |
} | |
set(searchParams: URLSearchParams, data: TInput) { | |
setData(searchParams, this.preprocess(data)); | |
} | |
} | |
interface Formatter<TInput extends object> { | |
format(data: TInput): string; | |
set(searchParams: URLSearchParams, data: TInput): void; | |
} | |
function setData(searchParams: URLSearchParams, data: object) { | |
Object.entries(data).forEach(([key, value]) => { | |
// `{ a: undefined }` should not change the `a` value. | |
if (value === undefined) { | |
return; | |
} | |
// `{ a: null }` should delete the `a` value. | |
searchParams.delete(key); | |
if (value == null) { | |
return; | |
} | |
if (Array.isArray(value) || value instanceof Set) { | |
return value.forEach((value) => { | |
searchParams.append(key, String(value)); | |
}); | |
} | |
searchParams.set(key, String(value)); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment