Skip to content

Commit

Permalink
feat: react18 legacy mode & react17 support (#50)
Browse files Browse the repository at this point in the history
Co-authored-by: Riri <[email protected]>
  • Loading branch information
jesse23 and Daydreamer-riri authored Dec 22, 2024
1 parent ef8ab7d commit 12d31df
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 39 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,11 @@ export default defineConfig({
})
```

### React17 Support

- for react18, with flag `useLegacyRender: true`, it will use the legacy `render` and `hydrate` methods.
- for react17, on top of above, you will need minor update to react and react-dom [example](https://github.com/jesse23/webpack-test-bed/blob/main/scripts/define-react-exports.js) to polyfill the mjs import and the `react-dom/client`.

## Roadmap

- [x] Preload assets
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vite-react-ssg",
"type": "module",
"version": "0.8.2",
"version": "0.8.3-beta.2",
"packageManager": "[email protected]",
"description": "Static-site generation for React on Vite.",
"author": "Riri <[email protected]>",
Expand Down Expand Up @@ -85,8 +85,8 @@
"beasties": "^0.1.0",
"critters": "^0.0.24",
"prettier": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react": "^17.0.2||^18.0.0",
"react-dom": "^17.0.2||^18.0.0",
"react-router-dom": "^6.14.1",
"styled-components": "^6.0.0",
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"
Expand Down
11 changes: 3 additions & 8 deletions src/client/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
import { HelmetProvider } from 'react-helmet-async'
import { RouterProvider, createBrowserRouter, matchRoutes } from 'react-router-dom'
import { hydrate, render } from '../pollfill/react-helper'
import type { RouteRecord, RouterOptions, ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
import { documentReady } from '../utils/document-ready'
import { deserializeState } from '../utils/state'
Expand Down Expand Up @@ -117,15 +117,10 @@ export function ViteReactSSG(
)
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
if (!isSSR && process.env.NODE_ENV === 'development') {
const root = ReactDOMCreateRoot(container)
React.startTransition(() => {
root.render(app)
})
render(app, container, options)
}
else {
React.startTransition(() => {
hydrateRoot(container, app)
})
hydrate(app, container, options)
}
})()
}
Expand Down
13 changes: 4 additions & 9 deletions src/client/single-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react'
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
import { HelmetProvider } from 'react-helmet-async'
import React from 'react'
import { hydrate, render } from '../pollfill/react-helper'
import type { ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
import { documentReady } from '../utils/document-ready'
import { deserializeState } from '../utils/state'
Expand Down Expand Up @@ -87,18 +87,13 @@ export function ViteReactSSG(
<HelmetProvider>
{App}
</HelmetProvider>
) as ReactNode
) as JSX.Element
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
if (!isSSR && process.env.NODE_ENV === 'development') {
const root = ReactDOMCreateRoot(container)
React.startTransition(() => {
root.render(app)
})
render(app, container, options)
}
else {
React.startTransition(() => {
hydrateRoot(container, app)
})
hydrate(app, container, options)
}
})()
}
Expand Down
32 changes: 15 additions & 17 deletions src/client/tanstack.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react'
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
import { HelmetProvider } from 'react-helmet-async'
import type { AnyContext, AnyRouter, LoaderFnContext } from '@tanstack/react-router'
import { RouterProvider } from '@tanstack/react-router'
import { Meta, StartClient } from '@tanstack/start'
import { hydrate, render } from '../pollfill/react-helper'
import type { ViteReactSSGContext as BaseViteReactSSGContext, ViteReactSSGClientOptions } from '../types'
import { documentReady } from '../utils/document-ready'
import { deserializeState } from '../utils/state'
Expand Down Expand Up @@ -172,24 +172,22 @@ export function Experimental_ViteReactSSG(
const { router } = await createRoot(true)
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
if (!isSSR && process.env.NODE_ENV === 'development') {
const root = ReactDOMCreateRoot(container)
React.startTransition(() => {
root.render(
<HelmetProvider>
<RouterProvider router={router} />
</HelmetProvider>,
)
})
render(
<HelmetProvider>
<RouterProvider router={router} />
</HelmetProvider>,
container,
options,
)
}
else {
React.startTransition(() => {
hydrateRoot(
container,
<HelmetProvider>
<StartClient router={router} />
</HelmetProvider>,
)
})
hydrate(
<HelmetProvider>
<StartClient router={router} />
</HelmetProvider>,
container,
options,
)
}
})()
}
Expand Down
9 changes: 7 additions & 2 deletions src/node/serverRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@

import { Writable } from 'node:stream'
import type { ReactNode } from 'react'
import { renderToPipeableStream } from 'react-dom/server'
import * as ReactDomServer from 'react-dom/server'

export async function renderStaticApp(app: ReactNode): Promise<string> {
// fallback to react17
if (!ReactDomServer.renderToPipeableStream) {
return ReactDomServer.renderToString(<>{app}</>)
};

// Inspired from
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
const writableStream = new WritableAsPromise()

const { pipe } = renderToPipeableStream(app, {
const { pipe } = ReactDomServer.renderToPipeableStream(app, {
onError(error) {
writableStream.destroy(error as Error)
},
Expand Down
72 changes: 72 additions & 0 deletions src/pollfill/react-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { ReactElement } from 'react'
import React from 'react'
import * as ReactDOM from 'react-dom'

export interface RootType {
render: (container: ReactElement) => void
_unmount: () => void
}
export interface RootTypeReact extends RootType {
unmount?: () => void
}
export type CreateRootFnType = (container: Element | DocumentFragment) => RootTypeReact

export type HydrateRootFnType = (container: Element | DocumentFragment, initialChildren: React.ReactNode) => RootTypeReact

const CopyReactDOM = {
...ReactDOM,
} as typeof ReactDOM & {
createRoot: CreateRootFnType
hydrateRoot: HydrateRootFnType
} & {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
usingClientEntryPoint: boolean
}
}

const { version, render: reactRender, hydrate: reactHydrate } = CopyReactDOM

const isReact18 = Number((version || '').split('.')[0]) > 17

interface RenderOptions {
useLegacyRender?: boolean
}

export function render(app: JSX.Element, container: Element | DocumentFragment, renderOptions: RenderOptions = {}) {
const { useLegacyRender } = renderOptions

if (useLegacyRender || !isReact18) {
reactRender(app, container)
}
else {
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true
const { createRoot } = CopyReactDOM
if (!createRoot) {
throw new Error('createRoot not found')
}
const root = createRoot(container)
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = false
React.startTransition(() => {
root.render(app)
})
}
}

export function hydrate(app: JSX.Element, container: Element | DocumentFragment, renderOptions: RenderOptions = {}) {
const { useLegacyRender } = renderOptions

if (useLegacyRender || !isReact18) {
reactHydrate(app, container)
}
else {
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true
const { hydrateRoot } = CopyReactDOM
if (!hydrateRoot) {
throw new Error('hydrateRoot not found')
}
React.startTransition(() => {
hydrateRoot(container, app)
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = false
})
}
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export interface ViteReactSSGClientOptions {
*/
ssrWhenDev?: boolean
getStyleCollector?: (() => StyleCollector | Promise<StyleCollector>) | null
// true if the app is based on react17 compatible API
useLegacyRender?: boolean
}

interface CommonRouteOptions {
Expand Down

0 comments on commit 12d31df

Please sign in to comment.