Skip to content
This repository has been archived by the owner on Nov 23, 2023. It is now read-only.

Commit

Permalink
Merge pull request #34 from fastify/react-tweaks
Browse files Browse the repository at this point in the history
fix(react): hydration order, streaming performance
  • Loading branch information
galvez authored Jul 1, 2022
2 parents 1f058e7 + 408ec75 commit efddd02
Show file tree
Hide file tree
Showing 7 changed files with 43 additions and 31 deletions.
26 changes: 13 additions & 13 deletions packages/fastify-dx-react/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,9 @@ export function createHtmlFunction (source, scope, config) {
const soFooterTemplate = createHtmlTemplateFunction(soFooterSource)
// This function gets registered as reply.html()
return function ({ routes, context, body }) {
// Initialize hydration, which can stay empty if context.serverOnly is true
let hydration = ''
// Decide which templating functions to use, with and without hydration
const headTemplate = context.serverOnly ? soHeadTemplate : unHeadTemplate
const footerTemplate = context.serverOnly ? soFooterTemplate : unFooterTemplate
// Decide whether or not to include the hydration script
if (!context.serverOnly) {
hydration = (
'<script>\n' +
`window.route = ${devalue(context.toJSON())}\n` +
`window.routes = ${devalue(routes.toJSON())}\n` +
'</script>'
)
}
// Render page-level <head> elements
const head = new Head(context.head).render()
// Create readable stream with prepended and appended chunks
Expand All @@ -78,8 +67,19 @@ export function createHtmlFunction (source, scope, config) {
? onShellReady(body)
: onAllReady(body)
),
head: headTemplate({ ...context, head, hydration }),
footer: footerTemplate(context),
head: headTemplate({ ...context, head }),
footer: () => footerTemplate({
...context,
// Decide whether or not to include the hydration script
...!context.serverOnly && {
hydration: (
'<script>\n' +
`window.route = ${devalue(context.toJSON())}\n` +
`window.routes = ${devalue(routes.toJSON())}\n` +
'</script>'
)
},
}),
}))
// Send out header and readable stream with full response
this.type('text/html')
Expand Down
7 changes: 4 additions & 3 deletions packages/fastify-dx-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"main": "index.js",
"name": "fastify-dx-react",
"version": "0.0.2",
"version": "0.0.3",
"files": [
"virtual/create.jsx",
"virtual/root.jsx",
Expand All @@ -27,10 +27,11 @@
"./plugin": "./plugin.cjs"
},
"dependencies": {
"devalue": "^2.0.1",
"minipass": "^3.3.4",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-router-dom": "^6.3.0",
"devalue": "^2.0.1",
"unihead": "^0.0.6",
"valtio": "^1.6.1"
},
Expand All @@ -45,4 +46,4 @@
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-react": "^7.29.4"
}
}
}
14 changes: 7 additions & 7 deletions packages/fastify-dx-react/server/stream.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

// Helper from the Node.js stream library to
// make it easier to work with renderToPipeableStream()
import { PassThrough } from 'stream'
// Helper to make the stream returned renderToPipeableStream()
// behave like an event emitter and facilitate error handling in Fastify
import Minipass from 'minipass'

// React 18's preferred server-side rendering function,
// which enables the combination of React.lazy() and Suspense
Expand All @@ -15,13 +15,13 @@ export async function * generateHtmlStream ({ head, body, footer }) {
yield chunk
}
}
yield footer
yield footer()
}

// Helper function to get an AsyncIterable (via PassThrough)
// from the renderToPipeableStream() onShellReady event
export function onShellReady (app) {
const duplex = new PassThrough()
const duplex = new Minipass()
return new Promise((resolve, reject) => {
try {
const pipeable = renderToPipeableStream(app, {
Expand All @@ -35,10 +35,10 @@ export function onShellReady (app) {
})
}

// Helper function to get an AsyncIterable (via PassThrough)
// Helper function to get an AsyncIterable (via Minipass)
// from the renderToPipeableStream() onAllReady event
export function onAllReady (app) {
const duplex = new PassThrough()
const duplex = new Minipass()
return new Promise((resolve, reject) => {
try {
const pipeable = renderToPipeableStream(app, {
Expand Down
15 changes: 10 additions & 5 deletions packages/fastify-dx-react/virtual/core.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import { proxy, useSnapshot } from 'valtio'
import { waitResource, waitFetch } from '/dx:resource.js'
import layouts from '/dx:layouts.js'

const isServer = typeof process === 'object'

export const isServer = import.meta.env.SSR
export const Router = isServer ? StaticRouter : BrowserRouter
export const RouteContext = createContext({})

export function useRouteContext () {
const routeContext = useContext(RouteContext)
if (routeContext.state) {
routeContext.snapshot = useSnapshot(routeContext.state)
routeContext.snapshot = isServer
? routeContext.state
: useSnapshot(routeContext.state)
}
return routeContext
}
Expand Down Expand Up @@ -56,7 +57,9 @@ export function DXRoute ({ head, ctxHydration, ctx, children }) {
<RouteContext.Provider value={{
...ctx,
...ctxHydration,
state: proxy(ctxHydration.state),
state: isServer
? ctxHydration.state
: proxy(ctxHydration.state),
}}>
<Layout>
{children}
Expand Down Expand Up @@ -130,7 +133,9 @@ export function DXRoute ({ head, ctxHydration, ctx, children }) {
<RouteContext.Provider value={{
...ctxHydration,
...ctx,
state: proxy(ctxHydration.state),
state: isServer
? ctxHydration.state
: proxy(ctxHydration.state),
}}>
<Layout>
{children}
Expand Down
2 changes: 1 addition & 1 deletion starters/react/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
<meta charset="utf-8">
<link rel="stylesheet" href="./base.css">
<!-- head -->
<!-- hydration -->
</head>
<body>
<main><!-- element --></main>
</body>
<!-- hydration -->
<script type="module" src="/dx:mount.js"></script>
</html>
8 changes: 7 additions & 1 deletion starters/react/client/pages/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logo from '/assets/logo.svg'
import { Link } from 'react-router-dom'
import { isServer, useRouteContext } from '/dx:core.jsx'

export function getMeta () {
return {
Expand All @@ -8,10 +9,15 @@ export function getMeta () {
}

export default function Index () {
const { snapshot, state } = useRouteContext()
if (isServer) {
// State is automatically hydrated on the client
state.message = 'Welcome to Fastify DX for React!'
}
return (
<>
<img src={logo} />
<h1>Welcome to Fastify DX for React!</h1>
<h1>{snapshot.message}</h1>
<ul className="columns-2">
<li><Link to="/using-data">/using-data</Link> demonstrates how to
leverage the <code>getData()</code> function
Expand Down
2 changes: 1 addition & 1 deletion starters/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint . --ext .js,.jsx --fix"
},
"dependencies": {
"fastify-dx-react": "^0.0.2",
"fastify-dx-react": "^0.0.3",
"fastify-vite": "^3.0.0-beta.23",
"ky-universal": "^0.10.1"
},
Expand Down

0 comments on commit efddd02

Please sign in to comment.