Skip to content

Instantly share code, notes, and snippets.

@kai-phan
Last active July 19, 2023 12:33
Show Gist options
  • Save kai-phan/824e6692a3a612da2cf7706beaa27277 to your computer and use it in GitHub Desktop.
Save kai-phan/824e6692a3a612da2cf7706beaa27277 to your computer and use it in GitHub Desktop.
Type safe useNavigate hook for React router 6.x
import {
Outlet,
createBrowserRouter,
useNavigate,
NavigateOptions,
} from 'react-router-dom';
const routeObjects = [
{
path: '/',
element: <Outlet />,
children: [
{
index: true,
lazy: () => import('./pages/home'),
},
{
path: ':id',
},
{
path: 'about',
children: [
{
index: true,
lazy: () => import('./pages/about'),
},
{
path: 'me',
lazy: () => import('./pages/me'),
},
{
path: ':id',
lazy: () => import('./pages/user_detail'),
},
{
path: ':id/:user_id',
},
{
path: '/us',
},
],
},
{
path: 'users',
index: true,
lazy: () => import('./pages/users'),
},
{
path: 'nested',
children: [
{
index: true,
lazy: () => import('./pages/nested'),
},
{
path: ':id',
children: [
{
index: true,
lazy: () => import('./pages/nested_id'),
},
{
path: 'nested_id_1',
},
{
path: 'nested_id_hai',
children: [
{
path: ':id_nested_id_hai_1',
},
],
},
],
},
{
path: 'nested_1',
children: [
{
index: true,
lazy: () => import('./pages/nested_1'),
},
{
path: 'nested_1_1',
children: [
{
index: true,
lazy: () => import('./pages/nested_1_1'),
},
],
},
{
path: ':nested_id',
children: [
{
path: 'nested_1_1',
},
],
},
],
},
],
},
],
},
{
path: '*',
},
] as const;
// Have to cast routeObjects to any because react-router-dom won't allow readyonly, and RouteObject[] won't let me infer the path
export default createBrowserRouter(routeObjects as any);
type AllPaths<A> = A extends readonly [infer F, ...infer R]
? F extends { path: infer P }
?
| (F extends { children: infer C }
? `${MakePaths<P>}/${MakePaths<AllPaths<C>>}` | P
: P)
| AllPaths<R>
: AllPaths<R>
: never;
type MakePaths<P> = P extends string
? P extends `/${infer U}`
? U
: P
: never;
type PathParams<
P,
R = NonNullable<unknown>,
V = number | string,
> = P extends `${string}:${infer T}/${infer U}`
? PathParams<U, R & { [K in T]: V }>
: P extends `:${infer T}` | `${string}:${infer T}`
? R & { [K in T]: V }
: R;
function replacePath(path: string, params: object) {
return Object.entries(params).reduce((acc, [key, value]) => {
return acc.replace(`:${key}`, String(value));
}, path);
}
type NavigateWithParams<T> = NavigateOptions & { params: PathParams<T> };
type SafeTo<T> = T | Partial<{ pathname: T; search: string; hash: string }>;
export function useSafeNavigate() {
const navigate = useNavigate();
function saveNavigate(delta: number): void;
function saveNavigate<T extends AllPaths<typeof routeObjects>>(
to: SafeTo<T>,
options?: NavigateWithParams<T>,
): void;
function saveNavigate(to: any, options?: any) {
if (typeof to === 'number') {
return navigate(to);
}
if (typeof to === 'object') {
const { pathname } = to;
const newPath = replacePath(pathname, options.params);
return navigate({ ...to, pathname: newPath }, options);
}
return navigate(replacePath(to, options.params), options);
}
return saveNavigate;
}
const navigate = useSafeNavigate();
navigate('/about/:id', { params: { id: 1 } });
navigate(
{
pathname: '/nested/:id/nested_id_hai/:id_nested_id_hai_1',
},
// Type error
{ params: {} },
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment