Created
January 7, 2025 09:28
-
-
Save BSoDium/ebc1eeb585449f3af61fe52e1d6a83d0 to your computer and use it in GitHub Desktop.
A convenient hook that allows using search parameters of the URL as if they were component states.
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 levenshtein from "fast-levenshtein"; | |
import { useCallback, useEffect, useState } from "react"; | |
import { NavigateOptions, useSearchParams } from "react-router-dom"; | |
export type SearchParamState<T> = [T, (value: T) => void]; | |
/** | |
* Read the value of a search parameter, or return undefined if it does not exist. | |
* This wrapper is necessary because URLSearchParams.get() returns null if the | |
* search parameter does not exist. | |
*/ | |
function read(searchParams: URLSearchParams, key: string): string | undefined { | |
const value = searchParams.get(key); | |
return value === null ? undefined : value; | |
} | |
/** | |
* Correct the value of a search parameter if it is not in the list of possible values. | |
* Uses Levenshtein distance to find the closest match. | |
*/ | |
function correct<T extends string>(value: string, values: readonly T[]): T { | |
if (values.includes(value as T)) { | |
return value as T; | |
} | |
let closestMatch = values[0]; | |
let closestDistance = levenshtein.get(value, closestMatch); | |
for (const possibleValue of values) { | |
const distance = levenshtein.get(value, possibleValue); | |
if (distance < closestDistance) { | |
closestDistance = distance; | |
closestMatch = possibleValue; | |
} | |
} | |
return closestMatch; | |
} | |
/** | |
* A hook that allows using search parameters of the URL as if they were component states. | |
* @param key The name of the search parameter. | |
* @param init The initial value of the search parameter. | |
* @param values An array of possible values for the search parameter, used to correct invalid values if provided. | |
* @returns The value of the search parameter and a function to set the value. | |
*/ | |
export function useSearchParamState<T extends string = string>( | |
key: string, | |
init?: T, | |
values?: readonly T[], | |
options?: NavigateOptions | |
): SearchParamState<T> { | |
const [searchParams, setSearchParams] = useSearchParams(); | |
const [searchParamState, setSearchParamState] = useState<T>( | |
(searchParams.has(key) ? read(searchParams, key) : init) as T | |
); | |
// Set the initial search parameter value. | |
useEffect( | |
() => { | |
if (read(searchParams, key) !== searchParamState && searchParamState) { | |
setSearchParams((searchParams) => { | |
searchParams.set(key, searchParamState); | |
return searchParams; | |
}, options); | |
} | |
}, | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
[key] | |
); | |
// Update the search parameter value. | |
const setState = useCallback( | |
(value: T) => { | |
setSearchParamState(value); | |
setSearchParams((searchParams) => { | |
if (value !== undefined) { | |
searchParams.set(key, value); | |
} else { | |
searchParams.delete(key); | |
} | |
return searchParams; | |
}, options); | |
}, | |
[key, options, setSearchParams] | |
); | |
// Update the state when the search parameter changes. | |
useEffect(() => { | |
const value = read(searchParams, key); | |
if (values !== undefined && value !== undefined) { | |
const correctedValue = correct(value, values); | |
if (correctedValue !== value) { | |
setState(correctedValue); | |
return; | |
} | |
} | |
setSearchParamState(value as T); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [searchParams]); | |
return [searchParamState, setState]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment