Skip to content

Instantly share code, notes, and snippets.

@BSoDium
Created January 7, 2025 09:28
Show Gist options
  • Save BSoDium/ebc1eeb585449f3af61fe52e1d6a83d0 to your computer and use it in GitHub Desktop.
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.
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