Improve useZodRouteParams (#1917)

* Better `useZodRouteParams`

* cleanup message
This commit is contained in:
Oscar Beaumont
2024-01-05 16:51:08 +08:00
committed by GitHub
parent f9853dad85
commit f7d135c5be
3 changed files with 53 additions and 20 deletions

View File

@@ -1,3 +1,4 @@
import { type Router } from '@remix-run/router';
import { createContext, useContext } from 'react';
import { createRoutes } from './app';
@@ -9,6 +10,9 @@ export const RoutingContext = createContext<{
routes: ReturnType<typeof createRoutes>;
} | null>(null);
// We split this into a different context because we don't want to trigger the hook unnecessarily
export const RouterContext = createContext<Router | null>(null);
export function useRoutingContext() {
const ctx = useContext(RoutingContext);
@@ -16,3 +20,10 @@ export function useRoutingContext() {
return ctx;
}
export function useRouter() {
const ctx = useContext(RouterContext);
if (!ctx) throw new Error('useRouter must be used within a RouterContext.Provider');
return ctx;
}

View File

@@ -1,9 +1,29 @@
import { useMemo } from 'react';
import { useParams } from 'react-router';
import { useEffect, useState } from 'react';
import type { z } from 'zod';
import { useRouter } from '~/RoutingContext';
// This is hook basically implements a custom version of `useParams`.
// If we use `useParams` directly, every time *any* param changes the current component will rerender so the hook reruns.
//
// With this improved implementation the component will only rerender if the change in parameter causes a change in the output of the Zod schema.
//
// We use this hook to get the library ID high up in the React tree so this reduces unnecessary rerenders of a large portion of the app.
export function useZodRouteParams<Z extends z.AnyZodObject>(schema: Z): z.infer<Z> {
// eslint-disable-next-line no-restricted-syntax
const params = useParams();
return useMemo(() => schema.parse(params), [params, schema]);
const router = useRouter();
const [result, setResult] = useState(() => {
const params = router.state.matches[router.state.matches.length - 1]?.params || {};
return schema.parse(params);
});
useEffect(
() =>
router.subscribe(({ matches }) => {
const routeMatch = matches[matches.length - 1];
const params = routeMatch ? (routeMatch.params as any) : {};
setResult(schema.parse(params));
}),
[router, schema, setResult]
);
return result;
}

View File

@@ -20,7 +20,7 @@ import { Devtools } from './components/Devtools';
import { WithPrismTheme } from './components/TextViewer/prism';
import ErrorFallback, { BetterErrorBoundary } from './ErrorFallback';
import { useTheme } from './hooks';
import { RoutingContext } from './RoutingContext';
import { RouterContext, RoutingContext } from './RoutingContext';
export * from './app';
export { ErrorPage } from './ErrorFallback';
@@ -53,21 +53,23 @@ export function SpacedriveRouterProvider(props: {
};
}) {
return (
<RoutingContext.Provider
value={{
routes: props.routing.routes,
visible: props.routing.visible,
currentIndex: props.routing.currentIndex,
maxIndex: props.routing.maxIndex
}}
>
<RouterProvider
router={props.routing.router}
future={{
v7_startTransition: true
<RouterContext.Provider value={props.routing.router}>
<RoutingContext.Provider
value={{
routes: props.routing.routes,
visible: props.routing.visible,
currentIndex: props.routing.currentIndex,
maxIndex: props.routing.maxIndex
}}
/>
</RoutingContext.Provider>
>
<RouterProvider
router={props.routing.router}
future={{
v7_startTransition: true
}}
/>
</RoutingContext.Provider>
</RouterContext.Provider>
);
}