-
-
Save Pagebakers/f39f154e5bb6b03c477cb4a1d88ac937 to your computer and use it in GitHub Desktop.
import { AnyZodObject, z } from 'zod' | |
import { Metadata, ResolvingMetadata } from 'next' | |
type InferParams<Params> = Params extends readonly string[] | |
? { | |
[K in Params[number]]: string | |
} | |
: Params extends AnyZodObject | |
? z.infer<Params> | |
: unknown | |
type LoaderFn< | |
Params extends readonly string[] | AnyZodObject, | |
SearchParams extends readonly string[] | AnyZodObject, | |
> = (args: { | |
params: InferParams<Params> | |
searchParams: InferParams<SearchParams> | |
}) => Promise<any> | |
type InferLoaderData<Loader> = Loader extends (args: any) => Promise<infer T> | |
? T | |
: unknown | |
export interface CreatePageProps< | |
Params extends readonly string[] | AnyZodObject, | |
SearchParams extends readonly string[] | AnyZodObject, | |
Loader extends LoaderFn<Params, SearchParams> = LoaderFn< | |
Params, | |
SearchParams | |
>, | |
> { | |
params?: Params | |
searchParams?: SearchParams | |
loader?: Loader | |
metadata?: | |
| Metadata | |
| (( | |
args: { | |
params: InferParams<Params> | |
searchParams: InferParams<SearchParams> | |
data: InferLoaderData<Loader> | |
}, | |
parent: ResolvingMetadata, | |
) => Promise<Metadata>) | |
component: React.ComponentType<{ | |
params: InferParams<Params> | |
searchParams?: InferParams<SearchParams> | |
data: InferLoaderData<Loader> | |
}> | |
} | |
function parseParams<Schema extends readonly string[] | AnyZodObject>( | |
params: Record<string, string>, | |
schema?: Schema, | |
) { | |
if (schema && 'parse' in schema) { | |
return schema.parse(params) as InferParams<Schema> | |
} | |
return params as InferParams<Schema> | |
} | |
export const createPage = < | |
const Params extends readonly string[] | AnyZodObject, | |
const SearchParams extends readonly string[] | AnyZodObject, | |
Loader extends LoaderFn<Params, SearchParams> = LoaderFn< | |
Params, | |
SearchParams | |
>, | |
>( | |
props: CreatePageProps<Params, SearchParams, Loader>, | |
) => { | |
const { | |
params: paramsSchema, | |
searchParams: searchParamsSchema, | |
component: PageComponent, | |
loader, | |
metadata, | |
} = props | |
// We don't really care about the types here since it's internal | |
async function Page(props: any) { | |
const params = parseParams(props.params, paramsSchema) | |
const searchParams = parseParams(props.searchParams, searchParamsSchema) | |
let pageProps: any = { | |
params, | |
searchParams, | |
} | |
if (typeof loader === 'function') { | |
const data = await loader(pageProps) | |
pageProps = { | |
...pageProps, | |
data, | |
} | |
} | |
return <PageComponent {...pageProps} /> | |
} | |
if (typeof metadata === 'function') { | |
return { | |
generateMetadata: async ( | |
{ | |
params, | |
searchParams, | |
}: { | |
params: InferParams<Params> | |
searchParams: InferParams<SearchParams> | |
}, | |
parent: ResolvingMetadata, | |
) => { | |
const data = | |
typeof loader === 'function' | |
? await loader({ | |
params, | |
searchParams, | |
}) | |
: undefined | |
return metadata( | |
{ | |
params, | |
searchParams, | |
data, | |
}, | |
parent, | |
) | |
}, | |
Page, | |
} | |
} | |
return { | |
metadata, | |
Page, | |
} | |
} |
Cool stuff! I’d recommend adding the capability to redirect if an error occurs in the loader (e.g., resource not found). This is a pattern that comes up again and again in my code:
// path: /pets/[petId] function MyPetPage(props: {petId: string}) { const query = usePetQuery(petId) const router = useRouter() useEffect(() => { if (!query.isLoading && !query.data) router.push("/pets") }, [query.isLoading, query.data]) if (!query.data) return <Loader /> return <MyPetPageInner pet={query.data} /> }This can all be tremendously simplified using your
createPage
wrapper, but there would still need to be a way to redirect if the pet doesn't exist.
You can do this in the component handler:
import { redirect } from 'next/navigation'
//...
component: ({data}) {
if (!data.workspace) {
redirect('/')
}
}
As for the questions you asked, my answers may not be very useful because I don’t use SSR nor the app router, but here goes anyways:
Use safeParse for the params and searchParams?
If the page needs a param to render and somehow it’s not there, then there’s not much you can do other than throw an error, so I don’t see what the benefits would be.
I agree params should be strict in this regard. But searchParams i'm not so sure, but then again, you can make your validation rules less strict in that case. The use case for not throwing an error would be to have more forgiving (for the end user) error handling in the UI, by keeping the UI functional instead just showing an error page.
Can we hydrate the data returned from the loader function easily to React Query?
You mean to pass the server-side loaded data to the client-side? Interesting… I don’t use React Query nor SSR but seems like that should be handled at a lower level, by React Query itself perhaps
Yep that would speed up initial page load a bit because the data doesnt need to be loaded after the client is finished loading. The result can be passed into initialData option of useQuery, but wondering if there's another way.
cache the loader function?
I would not want it to be cached as I’d want all cache to go into SWR/React Query.
Since the loader is called twice in this setup, in generateMetaData and in the component it would be beneficial, the data
FYI, you have a typo in your gist:
generateMetaData
should be generateMetadata
Thanks @magicspon , I've updated it.
Guess we need a next 15 version. Promisify all the things!
Cool stuff! I’d recommend adding the capability to redirect if an error occurs in the loader (e.g., resource not found). This is a pattern that comes up again and again in my code:
This can all be tremendously simplified using your
createPage
wrapper, but there would still need to be a way to redirect if the pet doesn't exist.As for the questions you asked, my answers may not be very useful because I don’t use SSR nor the app router, but here goes anyways:
Use safeParse for the params and searchParams?
If the page needs a param to render and somehow it’s not there, then there’s not much you can do other than throw an error, so I don’t see what the benefits would be.
Can we hydrate the data returned from the loader function easily to React Query?
You mean to pass the server-side loaded data to the client-side? Interesting… I don’t use React Query nor SSR but seems like that should be handled at a lower level, by React Query itself perhaps
cache the loader function?
I would not want it to be cached as I’d want all cache to go into SWR/React Query.