Skip to content

provide routes the option to have their props refetched each time the… #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions packages/fastify-renderer/src/client/react/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ const RouteTable = (props: {
)
}

export function Root<BootProps>(props: {
export function Root<BootProps extends Record<string, any>>(props: {
Entrypoint: React.FunctionComponent<BootProps>
Layout: React.FunctionComponent<LayoutProps>
bootProps: BootProps
basePath: string
routes: [string, React.FunctionComponent<any>][]
routes: [string, React.FunctionComponent<any>, any][]
}) {
const [firstRenderComplete, setFirstRenderComplete] = useState(false)
useEffect(() => {
Expand All @@ -43,28 +43,32 @@ export function Root<BootProps>(props: {
}, [])

const routes: JSX.Element[] = [
...props.routes.map(([route, Component]) => (
...props.routes.map(([route, Component, options]) => (
<Route path={route} key={route}>
{(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<string, any> }>(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<string, any> }>(
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 '#')
Expand Down
20 changes: 17 additions & 3 deletions packages/fastify-renderer/src/client/react/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,32 @@ const trackers: Record<string, Tracker<any>> = {}
/**
* 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 = <T>(key: string, promise: () => Promise<T>) => {
export const usePromise = <T>(key: string, promise: () => Promise<T>, 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
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/fastify-renderer/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const FastifyRenderer = fp<FastifyRendererOptions>(
...this[kRenderOptions],
pathPattern: routeOptions.url,
renderable,
options: routeOptions.renderableRouteOptions,
}

plugin.register(renderableRoute)
Expand Down
7 changes: 7 additions & 0 deletions packages/fastify-renderer/src/node/renderers/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:'
Expand Down Expand Up @@ -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) {
Expand All @@ -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')}
]
`
Expand All @@ -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')}
]`
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/fastify-renderer/src/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = (
this: FastifyInstance<Server, IncomingMessage, ServerResponse>,
Expand Down Expand Up @@ -54,6 +55,7 @@ declare module 'fastify' {

interface RouteShorthandOptions<RawServer extends RawServerBase = RawServerDefault> {
render?: string
renderableRouteOptions?: RenderableRouteOptions
}

interface FastifyRequest {
Expand Down