react-router
react-router copied to clipboard
[Feature]: useRequiredParams
What is the new or updated feature that you are suggesting?
Export a useRequiredParams hook, which accepts a list of the route parameters required by a component, throws an error if they are not set, and returns parameter variables with type string
.
Why should this feature be included?
The base useParams
hook returns param variables with type string | undefined
. This is the correct type definition, but commonly users of this hook are using it in a component they have no intention of rendering on a route without the route parameter in question. The string | undefined
type causes (for valid reasons) friction in the dev experience. There is discussion of this here.
On one of my projects at work this was bothering me and I put together a useRequiredParams hook which checks if they are set, throws a useful error if not, and returns params with type string
.
I think it would be helpful to include this alongside the base useParams hook (and just document that it will throw if a required param is unset).
I wrote up my own version here, but maybe a better implementation is possible. The point is that it throws a helpful error, and the hook name makes the intent explicit in contrast to useParams
.
Thanks, I implemented it slightly different so that a) only need to pass in the names as props, not as types, and b) not do the re-assignment since I don't mind if there are some other (optional) parameters:
import { useParams } from 'react-router-dom';
type RequiredParams<Key extends string = string> = {
readonly [key in Key]: string;
};
export const useRequiredParams = <Params extends string>(
requiredParamNames: Params[],
): RequiredParams<typeof requiredParamNames[number]> => {
const routeParams = useParams();
for (const paramName of requiredParamNames) {
const parameter = routeParams[paramName];
if (!parameter) {
throw new Error(
`This component should not be rendered on a route which does not have the ${paramName} parameter`,
);
}
}
return routeParams as RequiredParams<typeof requiredParamNames[number]>; // after looping through all required params, we can safely cast to the required type
};
I wanted to share my take as well, since we heavily use static-path in my company.
const useSafeParams = <Pattern extends string>(
path: Path<Pattern>
): Record<PathParamNames<Pattern>, string> => {
const routerParams = useParams();
return path.parts.reduce((currentParams, staticParam) => {
if (staticParam.kind === "param") {
const paramValue = routerParams[staticParam.paramName];
if (paramValue === undefined) {
/** Do something about it :) */
} else {
return {
...currentParams,
[staticParam.paramName]: paramValue
};
}
}
return currentParams;
/**
* We are kinda lying to TypeScript here, but the thing is that if you pass a path with no parameters to `useParams`,
* you will get a type error and therefore, this would never return an empty object (you can always bypass TypeScript, but should you?).
*/
}, {} as Record<PathParamNames<Pattern>, string>);
};
Usage would be something like
const moviePath = path("/movies/:movieId/comments/:commentId");
/** Nicely typed and safe! */
const { movieId, commentId } = useSafeParams(moviePath);
/** Whoops, you get a type error! seasonId does not exist in type { movieId: string, commentId: string } */
const { seasonId } = useSafeParams(moviePath);
Just sharing my case:
Add type guard with predicate params is RequiredParams<T>
which checks required param exists,
thus can assure required params exists and any other params might be optional.
- Just type/assure wrapper, so this way does not create new params object or manipulate value of params object from
useParams
.
type RouteParams = { [K in string]?: string };
type RequiredParams<Key extends string> = {
readonly [key in Key]: string;
} & Partial<Record<Exclude<string, Key>, string>>;
const hasRequiredParams = <T extends string>(
params: RouteParams,
requiredParamNames: readonly T[]
): params is RequiredParams<T> =>
requiredParamNames.every((paramName) => params[paramName] !== null && params[paramName] !== undefined);
const useRequiredParams = <T extends string>(requiredParamNames: readonly T[]): Readonly<RequiredParams<T>> => {
const routeParams = useParams<RouteParams>();
if (!hasRequiredParams(routeParams, requiredParamNames)) {
throw new Error(
[
`This component should not be rendered on a route since parameter is missing.`,
`- Required parameters: ${requiredParamNames.join(", ")}`,
`- Provided parameters: ${JSON.stringify(routeParams)}`,
].join("\n")
);
}
return routeParams;
};
// requiredParam: string
// someOtherParam: string | undefined -> defined by react-router
const { requiredParam, someOtherParam } = useRequiredParams(["requiredParam"]);
// managing requiredParamNames as array constant requires `as const`
// since if typeof requiredParamNames is string[], return type of useRequiredParams will be `RequiredParams<string>`
// which is why typeof requiredParamNames param is `readonly T[]` in useRequiredParams
const requiredParamNames = ["id", "name"] as const; // readonly ["id", "name"]
const { id, name } = useRequiredParams(requiredParamNames);
Would love to see something like this. any plans for it?
I'm going to convert this to a discussion so it can go through our new Open Development process. Please upvote the new Proposal if you'd like to see this considered!