Skip to content

Commit

Permalink
fix: create a component tree that matches between client and server t…
Browse files Browse the repository at this point in the history
…o avoid `useId` output mismatch (#371)

Co-authored-by: Marco Pasqualetti <[email protected]>
  • Loading branch information
Valerioageno and marcalexiei authored Jan 19, 2025
1 parent 43910fd commit 33aa37e
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 99 deletions.
2 changes: 2 additions & 0 deletions examples/tuono-app/src/routes/__layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode, JSX } from 'react'
import { TuonoScripts } from 'tuono'

interface RootLayoutProps {
children: ReactNode
Expand All @@ -9,6 +10,7 @@ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
<html>
<body>
<main>{children}</main>
<TuonoScripts />
</body>
</html>
)
Expand Down
76 changes: 38 additions & 38 deletions examples/tuono-app/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@ import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'

interface IndexProps {
subtitle: string
subtitle: string
}

export default function IndexPage({
data,
isLoading,
data,
isLoading,
}: TuonoProps<IndexProps>): JSX.Element {
if (isLoading) {
return <h1>Loading...</h1>
}
if (isLoading) {
return <h1>Loading...</h1>
}

return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<div className="subtitle-wrap">
<p className="subtitle">{data?.subtitle}</p>
<a
href="https://github.com/tuono-labs/tuono"
target="_blank"
className="button"
type="button"
>
Github
</a>
</div>
</>
)
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<div className="subtitle-wrap">
<p className="subtitle">{data?.subtitle}</p>
<a
href="https://github.com/tuono-labs/tuono"
target="_blank"
className="button"
type="button"
>
Github
</a>
</div>
</>
)
}
2 changes: 2 additions & 0 deletions examples/tuono-tutorial/src/routes/__layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode, JSX } from 'react'
import { TuonoScripts } from 'tuono'

interface RootLayoutProps {
children: ReactNode
Expand All @@ -9,6 +10,7 @@ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
<html>
<body>
<main>{children}</main>
<TuonoScripts />
</body>
</html>
)
Expand Down
2 changes: 2 additions & 0 deletions examples/with-mdx/src/routes/__layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ReactNode, JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
import { TuonoScripts } from 'tuono'

interface RootLayoutProps {
children: ReactNode
Expand All @@ -12,6 +13,7 @@ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
<main>
<MDXProvider components={{}}>{children}</MDXProvider>
</main>
<TuonoScripts />
</body>
</html>
)
Expand Down
2 changes: 2 additions & 0 deletions examples/with-tailwind/src/routes/__layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode, JSX } from 'react'
import { TuonoScripts } from 'tuono'

interface RootLayoutProps {
children: ReactNode
Expand All @@ -12,6 +13,7 @@ export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
</head>
<body>
<main>{children}</main>
<TuonoScripts />
</body>
</html>
)
Expand Down
15 changes: 10 additions & 5 deletions packages/tuono-router/src/components/RouterContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { createContext, useState, useEffect, useContext, useMemo } from 'react'
import type { ReactNode } from 'react'

import type { Router } from '../router'
import type { ServerRouterInfo } from '../types'
import type { ServerRouterInfo, ServerProps } from '../types'

const isServerSide = typeof window === 'undefined'

export interface ParsedLocation {
href: string
Expand All @@ -15,6 +17,7 @@ export interface ParsedLocation {
interface RouterContextValue {
router: Router
location: ParsedLocation
serverSideProps?: ServerProps
updateLocation: (loc: ParsedLocation) => void
}

Expand Down Expand Up @@ -48,7 +51,7 @@ function getInitialLocation(
interface RouterContextProviderProps {
router: Router
children: ReactNode
serverSideProps?: ServerRouterInfo
serverSideProps?: ServerProps
}

export function RouterContextProvider({
Expand All @@ -60,7 +63,7 @@ export function RouterContextProvider({
router.update({ ...router.options } as Parameters<typeof router.update>[0])

const [location, setLocation] = useState<ParsedLocation>(() =>
getInitialLocation(serverSideProps),
getInitialLocation(serverSideProps?.router),
)

/**
Expand Down Expand Up @@ -91,11 +94,14 @@ export function RouterContextProvider({

const contextValue: RouterContextValue = useMemo(
() => ({
serverSideProps: isServerSide
? serverSideProps
: window.__TUONO_SSR_PROPS__,
router,
location,
updateLocation: setLocation,
}),
[location, router],
[location, router, serverSideProps],
)

return (
Expand All @@ -105,7 +111,6 @@ export function RouterContextProvider({
)
}

/** @warning DO NOT EXPORT THIS TO USER LAND */
export function useRouterContext(): RouterContextValue {
return useContext(RouterContext)
}
5 changes: 1 addition & 4 deletions packages/tuono-router/src/components/RouterProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export function RouterProvider({
serverProps,
}: RouterProviderProps): JSX.Element {
return (
<RouterContextProvider
router={router}
serverSideProps={serverProps?.router}
>
<RouterContextProvider router={router} serverSideProps={serverProps}>
<Matches serverSideProps={serverProps?.props} />
</RouterContextProvider>
)
Expand Down
5 changes: 2 additions & 3 deletions packages/tuono-router/src/globals.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Router } from './router'
import type { ServerProps } from './types'

declare global {
interface Window {
__TUONO__ROUTER__: Router
__TUONO_SSR_PROPS__?: {
props?: unknown
}
__TUONO_SSR_PROPS__?: ServerProps
}
}
1 change: 1 addition & 0 deletions packages/tuono-router/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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'
Expand Down
3 changes: 3 additions & 0 deletions packages/tuono-router/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
6 changes: 3 additions & 3 deletions packages/tuono/src/hydration/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import { StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
Expand All @@ -11,8 +11,8 @@ export function hydrate(routeTree: RouteTree): void {

hydrateRoot(
document,
<React.StrictMode>
<StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
</StrictMode>,
)
}
8 changes: 6 additions & 2 deletions packages/tuono/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export {
useRouter,
} 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'
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const SCRIPT_BASE_URL = `http://localhost:${TUONO_DEV_SERVER_PORT}${VITE_PROXY_P

export const DevResources = (): JSX.Element => (
<>
<script type="module">
<script type="module" async>
{[
`import RefreshRuntime from '${SCRIPT_BASE_URL}/@react-refresh'`,
'RefreshRuntime.injectIntoGlobalHook(window)',
Expand All @@ -15,7 +15,15 @@ export const DevResources = (): JSX.Element => (
'window.__vite_plugin_react_preamble_installed__ = true',
].join('\n')}
</script>
<script type="module" src={`${SCRIPT_BASE_URL}/@vite/client`}></script>
<script type="module" src={`${SCRIPT_BASE_URL}/client-main.tsx`}></script>
<script
type="module"
async
src={`${SCRIPT_BASE_URL}/@vite/client`}
></script>
<script
type="module"
async
src={`${SCRIPT_BASE_URL}/client-main.tsx`}
></script>
</>
)
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>
))}
</>
)
}
18 changes: 18 additions & 0 deletions packages/tuono/src/shared/TuonoScripts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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
}
Loading

0 comments on commit 33aa37e

Please sign in to comment.