From f7d135c5beb0ded110a202c540dfbadfa2308f8b Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Fri, 5 Jan 2024 16:51:08 +0800 Subject: [PATCH] Improve `useZodRouteParams` (#1917) * Better `useZodRouteParams` * cleanup message --- interface/RoutingContext.tsx | 11 ++++++++++ interface/hooks/useZodRouteParams.ts | 30 +++++++++++++++++++++----- interface/index.tsx | 32 +++++++++++++++------------- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/interface/RoutingContext.tsx b/interface/RoutingContext.tsx index ae182590a..83c113a4f 100644 --- a/interface/RoutingContext.tsx +++ b/interface/RoutingContext.tsx @@ -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; } | null>(null); +// We split this into a different context because we don't want to trigger the hook unnecessarily +export const RouterContext = createContext(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; +} diff --git a/interface/hooks/useZodRouteParams.ts b/interface/hooks/useZodRouteParams.ts index f4f6c3519..03e5185f9 100644 --- a/interface/hooks/useZodRouteParams.ts +++ b/interface/hooks/useZodRouteParams.ts @@ -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(schema: Z): z.infer { - // 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; } diff --git a/interface/index.tsx b/interface/index.tsx index e125b33fc..789bf2088 100644 --- a/interface/index.tsx +++ b/interface/index.tsx @@ -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 ( - - + - + > + + + ); }