diff --git a/packages/fastify-renderer/src/client/react/Root.tsx b/packages/fastify-renderer/src/client/react/Root.tsx index db46ade3..020a858b 100644 --- a/packages/fastify-renderer/src/client/react/Root.tsx +++ b/packages/fastify-renderer/src/client/react/Root.tsx @@ -25,12 +25,12 @@ const RouteTable = (props: { ) } -export function Root(props: { +export function Root>(props: { Entrypoint: React.FunctionComponent Layout: React.FunctionComponent bootProps: BootProps basePath: string - routes: [string, React.FunctionComponent][] + routes: [string, React.FunctionComponent, any][] }) { const [firstRenderComplete, setFirstRenderComplete] = useState(false) useEffect(() => { @@ -43,28 +43,32 @@ export function Root(props: { }, []) const routes: JSX.Element[] = [ - ...props.routes.map(([route, Component]) => ( + ...props.routes.map(([route, Component, options]) => ( {(params) => { const [location] = useLocation() const router = useRouter() const backendPath = location.split('#')[0] // remove current anchor for fetching data from the server side - const payload = usePromise<{ props: Record }>(props.basePath + backendPath, async () => { - if (!firstRenderComplete) { - return { props: props.bootProps } - } else { - return ( - await fetch(props.basePath + backendPath, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - credentials: 'same-origin', - }) - ).json() - } - }) + const payload = usePromise<{ props: Record }>( + props.basePath + backendPath, + async () => { + if (!firstRenderComplete) { + return { props: props.bootProps } + } else { + return ( + await fetch(props.basePath + backendPath, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + credentials: 'same-origin', + }) + ).json() + } + }, + { refetch: options?.refetch } + ) // Navigate to the anchor in the url after rendering, unless we're using replaceState and // the destination page and previous page have the same base route (i.e. before '#') diff --git a/packages/fastify-renderer/src/client/react/fetcher.ts b/packages/fastify-renderer/src/client/react/fetcher.ts index 2eba0837..85200d15 100644 --- a/packages/fastify-renderer/src/client/react/fetcher.ts +++ b/packages/fastify-renderer/src/client/react/fetcher.ts @@ -9,18 +9,32 @@ const trackers: Record> = {} /** * Implements a React Suspense cache for a promise. For each passed `key` passed, runs the promise on first invocation, stores it, and suspends. On the next invocaation, will synchronously return the result of the promise if it resolved, or throw it's reject reason if it rejected. **/ -export const usePromise = (key: string, promise: () => Promise) => { +export const usePromise = (key: string, promise: () => Promise, options?: { refetch?: boolean }) => { + const refetch = options?.refetch ?? false + let tracker = trackers[key] if (tracker) { // If an error occurred, if (Object.prototype.hasOwnProperty.call(tracker, 'error')) { - throw tracker.error + const error = tracker.error + + if (refetch) { + delete trackers[key] + } + + throw error } // If a response was successful, if (Object.prototype.hasOwnProperty.call(tracker, 'response')) { - return tracker.response + const response = tracker.response + + if (refetch) { + delete trackers[key] + } + + return response } } diff --git a/packages/fastify-renderer/src/node/index.ts b/packages/fastify-renderer/src/node/index.ts index a2e7af4e..fe2dd9e8 100644 --- a/packages/fastify-renderer/src/node/index.ts +++ b/packages/fastify-renderer/src/node/index.ts @@ -124,6 +124,7 @@ const FastifyRenderer = fp( ...this[kRenderOptions], pathPattern: routeOptions.url, renderable, + options: routeOptions.renderableRouteOptions, } plugin.register(renderableRoute) diff --git a/packages/fastify-renderer/src/node/renderers/Renderer.ts b/packages/fastify-renderer/src/node/renderers/Renderer.ts index 29e78be4..7a68c355 100644 --- a/packages/fastify-renderer/src/node/renderers/Renderer.ts +++ b/packages/fastify-renderer/src/node/renderers/Renderer.ts @@ -9,6 +9,12 @@ export interface RenderOptions { base: string } +/** The options configuring a renderable route */ +export interface RenderableRouteOptions { + /** If the component rendered by the route should have its props refetched each time it is rendered */ + refetch?: boolean +} + export type PartialRenderOptions = | { layout: string; document: Template; base: string } | { layout: string; base: string } @@ -24,6 +30,7 @@ export interface RenderableRegistration extends RenderOptions { renderable: string /** If this renderable was registered for imperative rendering */ isImperative?: true + options?: RenderableRouteOptions } /** A unit of renderable work */ diff --git a/packages/fastify-renderer/src/node/renderers/react/ReactRenderer.tsx b/packages/fastify-renderer/src/node/renderers/react/ReactRenderer.tsx index 85bcdc3a..1cd85443 100644 --- a/packages/fastify-renderer/src/node/renderers/react/ReactRenderer.tsx +++ b/packages/fastify-renderer/src/node/renderers/react/ReactRenderer.tsx @@ -10,7 +10,7 @@ import { RenderBus } from '../../RenderBus' import { wrap } from '../../tracing' import { FastifyRendererHook } from '../../types' import { mapFilepathToEntrypointName, unthunk } from '../../utils' -import { Render, RenderableRegistration, Renderer, scriptTag } from '../Renderer' +import { Render, RenderableRegistration, RenderableRouteOptions, Renderer, scriptTag } from '../Renderer' const CLIENT_ENTRYPOINT_PREFIX = '/@fstr!entrypoint:' const SERVER_ENTRYPOINT_PREFIX = '/@fstr!server-entrypoint:' @@ -457,9 +457,10 @@ export class ReactRenderer implements Renderer { routeableRenderables.sort((a, b) => routeSortScore(a.pathPattern) - routeSortScore(b.pathPattern)) - const pathsToModules = routeableRenderables.map((route) => [ + const pathsToModules: [string, string, RenderableRouteOptions][] = routeableRenderables.map((route) => [ pathToRegexpify(this.stripBasePath(route.pathPattern, base)), route.renderable, + route.options || {}, ]) if (lazy) { @@ -468,7 +469,10 @@ import { lazy } from "react"; // lazy route table generated by fastify-renderer export const routes = [ ${pathsToModules - .map(([url, component]) => `[${JSON.stringify(url)}, lazy(() => import(${JSON.stringify(component)}))]`) + .map( + ([url, component, options]) => + `[${JSON.stringify(url)}, lazy(() => import(${JSON.stringify(component)})), ${JSON.stringify(options)}]` + ) .join(',\n')} ] ` @@ -478,7 +482,9 @@ export const routes = [ ${pathsToModules.map(([_url, component], index) => `import mod_${index} from ${JSON.stringify(component)}`).join('\n')} export const routes = [ - ${pathsToModules.map(([url], index) => `[${JSON.stringify(url)}, mod_${index}]`).join(',\n')} + ${pathsToModules + .map(([url, _, options], index) => `[${JSON.stringify(url)}, mod_${index}, ${JSON.stringify(options)}]`) + .join(',\n')} ]` } } diff --git a/packages/fastify-renderer/src/node/types.ts b/packages/fastify-renderer/src/node/types.ts index 59c588d7..32755d68 100644 --- a/packages/fastify-renderer/src/node/types.ts +++ b/packages/fastify-renderer/src/node/types.ts @@ -14,6 +14,7 @@ import { IncomingMessage, Server, ServerResponse } from 'http' import { ReactElement } from 'react' import { ViteDevServer } from 'vite' import { ImperativeRenderable } from './Plugin' +import { RenderableRouteOptions } from './renderers/Renderer' export type ServerRenderer = ( this: FastifyInstance, @@ -54,6 +55,7 @@ declare module 'fastify' { interface RouteShorthandOptions { render?: string + renderableRouteOptions?: RenderableRouteOptions } interface FastifyRequest {