Skip to content
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

fix: create a component tree that matches between client and server to avoid useId output mismatch #371

Merged
merged 11 commits into from
Jan 19, 2025
Prev Previous commit
Next Next commit
implement prod resources
Valerioageno committed Jan 19, 2025
commit 5c32ff25a3e69a88b88efab2f7bad111081a97a8
26 changes: 0 additions & 26 deletions packages/tuono-router/src/TuonoScripts.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion packages/tuono-router/src/components/RouterContext.tsx
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react'
import type { Router } from '../router'
import type { ServerRouterInfo, ServerProps } from '../types'

const isServer = typeof document === 'undefined'
const isServer = typeof window === 'undefined'

export interface ParsedLocation {
href: string
2 changes: 1 addition & 1 deletion packages/tuono-router/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export { RouterProvider } from './components/RouterProvider'
export { useRouterContext } from './components/RouterContext'
export { default as Link } from './components/Link'
export { createRouter } from './router'
export { createRoute, createRootRoute } from './route'
export { useRouter } from './hooks/useRouter'
export type { RouteProps, RouteComponent } from './types'
export { TuonoScripts } from './TuonoScripts'
3 changes: 3 additions & 0 deletions packages/tuono-router/src/types.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,9 @@ export interface ServerRouterInfo {
export interface ServerProps<TProps = unknown> {
router: ServerRouterInfo
props: TProps
jsBundles: Array<string>
cssBundles: Array<string>
mode: 'Dev' | 'Prod'
}

export interface RouteProps<TData = unknown> {
9 changes: 6 additions & 3 deletions packages/tuono/src/index.ts
Original file line number Diff line number Diff line change
@@ -4,10 +4,13 @@ export {
createRouter,
Link,
useRouter,
TuonoScripts,
} from 'tuono-router'

export { RouteLazyLoading as __tuono__internal__lazyLoadRoute } from './dynamic/RouteLazyLoading'
export { dynamic } from './dynamic/dynamic'
export {
dynamic,
RouteLazyLoading as __tuono__internal__lazyLoadRoute,
} from './shared/dynamic'

export { TuonoScripts } from './shared/TuonoScripts'

export type { TuonoProps } from './types'
24 changes: 24 additions & 0 deletions packages/tuono/src/shared/ProdResources.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { JSX } from 'react'
import { useRouterContext } from 'tuono-router'

export const ProdResources = (): JSX.Element => {
const { serverSideProps } = useRouterContext()

return (
<>
{serverSideProps?.cssBundles.map((cssHref) => (
<link
key={cssHref}
rel="stylesheet"
precedence="high"
type="text/css"
href={`/${cssHref}`}
/>
))}

{serverSideProps?.jsBundles.map((scriptSrc) => (
<script key={scriptSrc} type="module" src={`/${scriptSrc}`}></script>
))}
</>
)
}
16 changes: 16 additions & 0 deletions packages/tuono/src/shared/TuonoScripts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { JSX } from 'react'
import { useRouterContext } from 'tuono-router'
import { DevResources } from './DevResources'
import { ProdResources } from './ProdResources'

export function TuonoScripts(): JSX.Element {
const { serverSideProps } = useRouterContext()

return (
<>
<script>{`window.__TUONO_SSR_PROPS__=${JSON.stringify(serverSideProps)}`}</script>
{serverSideProps?.mode === 'Dev' && <DevResources />}
{serverSideProps?.mode === 'Prod' && <ProdResources />}
</>
)
}
24 changes: 24 additions & 0 deletions packages/tuono/src/shared/dynamic/RouteLazyLoading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { lazy, createElement } from 'react'
import type { ReactElement } from 'react'

import type { RouteComponent } from 'tuono-router'

type ImportFn = () => Promise<{ default: RouteComponent }>

export const RouteLazyLoading = (factory: ImportFn): RouteComponent => {
let LoadedComponent: RouteComponent | undefined
const LazyComponent = lazy<RouteComponent>(factory)

const loadComponent = (): Promise<void> =>
factory().then((module) => {
LoadedComponent = module.default
})

const Component = (
props: React.ComponentProps<RouteComponent>,
): ReactElement => createElement(LoadedComponent || LazyComponent, props)

Component.preload = loadComponent

return Component
}
88 changes: 88 additions & 0 deletions packages/tuono/src/shared/dynamic/dynamic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* This component is heavily inspired by Next.js dynamic function
* Link: https://github.com/vercel/next.js/blob/1df81bcea62800198884438a2bb27ba14c9d506a/packages/next/src/shared/lib/dynamic.tsx
*/
import { lazy, Suspense, Fragment } from 'react'
import type { ComponentType } from 'react'

const isServerSide = typeof window === 'undefined'

interface ComponentModule<T> {
default: React.ComponentType<T>
}

interface DynamicOptions {
ssr?: boolean
loading?: React.ComponentType<unknown> | null
}

type Loader<T = object> = () => Promise<
React.ComponentType<T> | ComponentModule<T>
>

interface LoadableOptions<T> extends DynamicOptions {
loader: Loader<T>
}

type LoadableFn = <T = object>(options: LoadableOptions<T>) => ComponentType<T>

const defaultLoaderOptions: LoadableOptions<object> = {
ssr: true,
loading: null,
loader: () => Promise.resolve(() => null),
}

function noSSR<T = object>(
LoadableInitializer: LoadableFn,
loadableOptions: LoadableOptions<T>,
): React.ComponentType<T> {
if (!isServerSide) {
return LoadableInitializer(loadableOptions)
}

if (!loadableOptions.loading) return () => null

const Loading = loadableOptions.loading
// This will only be rendered on the server side
function NoSSRLoading(): React.JSX.Element {
return <Loading />
}
return NoSSRLoading
}

function Loadable<T = object>(options: LoadableOptions<T>): ComponentType<T> {
const opts = { ...defaultLoaderOptions, ...options }
const Lazy = lazy(() => opts.loader().then())
const Loading = opts.loading

function LoadableComponent(props: T): React.JSX.Element {
const fallbackElement = Loading ? <Loading /> : null

const Wrap = Loading ? Suspense : Fragment
const wrapProps = Loading ? { fallback: fallbackElement } : {}

// TODO: In case ssr = false handle also the assets preloading
return (
<Wrap {...wrapProps}>
<Lazy {...props} />
</Wrap>
)
}
LoadableComponent.displayName = 'LoadableComponent'

return LoadableComponent
}

/**
* This function lets you dynamically import a component.
* It uses [React.lazy()](https://react.dev/reference/react/lazy) with [Suspense](https://react.dev/reference/react/Suspense) under the hood.
*/
export function dynamic<T = object>(
importFn: Loader<T>,
opts?: DynamicOptions,
): ComponentType<T> {
if (typeof opts?.ssr === 'boolean' && !opts.ssr) {
return noSSR<T>(Loadable, { ...opts, loader: importFn })
}
return Loadable<T>({ ...opts, loader: importFn })
}
2 changes: 2 additions & 0 deletions packages/tuono/src/shared/dynamic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { dynamic } from './dynamic'
export { RouteLazyLoading } from './RouteLazyLoading'
26 changes: 0 additions & 26 deletions packages/tuono/src/ssr/components/ProdResources.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions packages/tuono/src/ssr/server.tsx
Original file line number Diff line number Diff line change
@@ -44,9 +44,6 @@ import { renderToReadableStream } from 'react-dom/server'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'

import { DevResources } from './components/DevResources'
import { ProdResources } from './components/ProdResources'
import type { Mode } from './types'
import { streamToString } from './utils'

type RouteTree = ReturnType<typeof createRoute>
@@ -58,9 +55,6 @@ export function serverSideRendering(routeTree: RouteTree) {
unknown
>

const mode = serverProps.mode as Mode
const jsBundles = serverProps.jsBundles as Array<string>
const cssBundles = serverProps.cssBundles as Array<string>
const router = createRouter({ routeTree }) // Render the app

const stream = await renderToReadableStream(