Skip to content

Commit 12d31df

Browse files
feat: react18 legacy mode & react17 support (#50)
Co-authored-by: Riri <Daydreamerriri@outlook.com>
1 parent ef8ab7d commit 12d31df

File tree

8 files changed

+111
-39
lines changed

8 files changed

+111
-39
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,11 @@ export default defineConfig({
609609
})
610610
```
611611

612+
### React17 Support
613+
614+
- for react18, with flag `useLegacyRender: true`, it will use the legacy `render` and `hydrate` methods.
615+
- 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`.
616+
612617
## Roadmap
613618

614619
- [x] Preload assets

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vite-react-ssg",
33
"type": "module",
4-
"version": "0.8.2",
4+
"version": "0.8.3-beta.2",
55
"packageManager": "pnpm@9.12.3",
66
"description": "Static-site generation for React on Vite.",
77
"author": "Riri <Daydreamerriri@outlook.com>",
@@ -85,8 +85,8 @@
8585
"beasties": "^0.1.0",
8686
"critters": "^0.0.24",
8787
"prettier": "*",
88-
"react": "^18.0.0",
89-
"react-dom": "^18.0.0",
88+
"react": "^17.0.2||^18.0.0",
89+
"react-dom": "^17.0.2||^18.0.0",
9090
"react-router-dom": "^6.14.1",
9191
"styled-components": "^6.0.0",
9292
"vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0"

src/client/index.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
2-
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
32
import { HelmetProvider } from 'react-helmet-async'
43
import { RouterProvider, createBrowserRouter, matchRoutes } from 'react-router-dom'
4+
import { hydrate, render } from '../pollfill/react-helper'
55
import type { RouteRecord, RouterOptions, ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
66
import { documentReady } from '../utils/document-ready'
77
import { deserializeState } from '../utils/state'
@@ -117,15 +117,10 @@ export function ViteReactSSG(
117117
)
118118
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
119119
if (!isSSR && process.env.NODE_ENV === 'development') {
120-
const root = ReactDOMCreateRoot(container)
121-
React.startTransition(() => {
122-
root.render(app)
123-
})
120+
render(app, container, options)
124121
}
125122
else {
126-
React.startTransition(() => {
127-
hydrateRoot(container, app)
128-
})
123+
hydrate(app, container, options)
129124
}
130125
})()
131126
}

src/client/single-page.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactNode } from 'react'
2-
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
32
import { HelmetProvider } from 'react-helmet-async'
43
import React from 'react'
4+
import { hydrate, render } from '../pollfill/react-helper'
55
import type { ViteReactSSGClientOptions, ViteReactSSGContext } from '../types'
66
import { documentReady } from '../utils/document-ready'
77
import { deserializeState } from '../utils/state'
@@ -87,18 +87,13 @@ export function ViteReactSSG(
8787
<HelmetProvider>
8888
{App}
8989
</HelmetProvider>
90-
) as ReactNode
90+
) as JSX.Element
9191
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
9292
if (!isSSR && process.env.NODE_ENV === 'development') {
93-
const root = ReactDOMCreateRoot(container)
94-
React.startTransition(() => {
95-
root.render(app)
96-
})
93+
render(app, container, options)
9794
}
9895
else {
99-
React.startTransition(() => {
100-
hydrateRoot(container, app)
101-
})
96+
hydrate(app, container, options)
10297
}
10398
})()
10499
}

src/client/tanstack.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react'
2-
import { createRoot as ReactDOMCreateRoot, hydrateRoot } from 'react-dom/client'
32
import { HelmetProvider } from 'react-helmet-async'
43
import type { AnyContext, AnyRouter, LoaderFnContext } from '@tanstack/react-router'
54
import { RouterProvider } from '@tanstack/react-router'
65
import { Meta, StartClient } from '@tanstack/start'
6+
import { hydrate, render } from '../pollfill/react-helper'
77
import type { ViteReactSSGContext as BaseViteReactSSGContext, ViteReactSSGClientOptions } from '../types'
88
import { documentReady } from '../utils/document-ready'
99
import { deserializeState } from '../utils/state'
@@ -172,24 +172,22 @@ export function Experimental_ViteReactSSG(
172172
const { router } = await createRoot(true)
173173
const isSSR = document.querySelector('[data-server-rendered=true]') !== null
174174
if (!isSSR && process.env.NODE_ENV === 'development') {
175-
const root = ReactDOMCreateRoot(container)
176-
React.startTransition(() => {
177-
root.render(
178-
<HelmetProvider>
179-
<RouterProvider router={router} />
180-
</HelmetProvider>,
181-
)
182-
})
175+
render(
176+
<HelmetProvider>
177+
<RouterProvider router={router} />
178+
</HelmetProvider>,
179+
container,
180+
options,
181+
)
183182
}
184183
else {
185-
React.startTransition(() => {
186-
hydrateRoot(
187-
container,
188-
<HelmetProvider>
189-
<StartClient router={router} />
190-
</HelmetProvider>,
191-
)
192-
})
184+
hydrate(
185+
<HelmetProvider>
186+
<StartClient router={router} />
187+
</HelmetProvider>,
188+
container,
189+
options,
190+
)
193191
}
194192
})()
195193
}

src/node/serverRenderer.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77

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

1212
export async function renderStaticApp(app: ReactNode): Promise<string> {
13+
// fallback to react17
14+
if (!ReactDomServer.renderToPipeableStream) {
15+
return ReactDomServer.renderToString(<>{app}</>)
16+
};
17+
1318
// Inspired from
1419
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
1520
// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby/cache-dir/static-entry.js
1621
const writableStream = new WritableAsPromise()
1722

18-
const { pipe } = renderToPipeableStream(app, {
23+
const { pipe } = ReactDomServer.renderToPipeableStream(app, {
1924
onError(error) {
2025
writableStream.destroy(error as Error)
2126
},

src/pollfill/react-helper.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { ReactElement } from 'react'
2+
import React from 'react'
3+
import * as ReactDOM from 'react-dom'
4+
5+
export interface RootType {
6+
render: (container: ReactElement) => void
7+
_unmount: () => void
8+
}
9+
export interface RootTypeReact extends RootType {
10+
unmount?: () => void
11+
}
12+
export type CreateRootFnType = (container: Element | DocumentFragment) => RootTypeReact
13+
14+
export type HydrateRootFnType = (container: Element | DocumentFragment, initialChildren: React.ReactNode) => RootTypeReact
15+
16+
const CopyReactDOM = {
17+
...ReactDOM,
18+
} as typeof ReactDOM & {
19+
createRoot: CreateRootFnType
20+
hydrateRoot: HydrateRootFnType
21+
} & {
22+
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
23+
usingClientEntryPoint: boolean
24+
}
25+
}
26+
27+
const { version, render: reactRender, hydrate: reactHydrate } = CopyReactDOM
28+
29+
const isReact18 = Number((version || '').split('.')[0]) > 17
30+
31+
interface RenderOptions {
32+
useLegacyRender?: boolean
33+
}
34+
35+
export function render(app: JSX.Element, container: Element | DocumentFragment, renderOptions: RenderOptions = {}) {
36+
const { useLegacyRender } = renderOptions
37+
38+
if (useLegacyRender || !isReact18) {
39+
reactRender(app, container)
40+
}
41+
else {
42+
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true
43+
const { createRoot } = CopyReactDOM
44+
if (!createRoot) {
45+
throw new Error('createRoot not found')
46+
}
47+
const root = createRoot(container)
48+
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = false
49+
React.startTransition(() => {
50+
root.render(app)
51+
})
52+
}
53+
}
54+
55+
export function hydrate(app: JSX.Element, container: Element | DocumentFragment, renderOptions: RenderOptions = {}) {
56+
const { useLegacyRender } = renderOptions
57+
58+
if (useLegacyRender || !isReact18) {
59+
reactHydrate(app, container)
60+
}
61+
else {
62+
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = true
63+
const { hydrateRoot } = CopyReactDOM
64+
if (!hydrateRoot) {
65+
throw new Error('hydrateRoot not found')
66+
}
67+
React.startTransition(() => {
68+
hydrateRoot(container, app)
69+
CopyReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.usingClientEntryPoint = false
70+
})
71+
}
72+
}

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export interface ViteReactSSGClientOptions {
168168
*/
169169
ssrWhenDev?: boolean
170170
getStyleCollector?: (() => StyleCollector | Promise<StyleCollector>) | null
171+
// true if the app is based on react17 compatible API
172+
useLegacyRender?: boolean
171173
}
172174

173175
interface CommonRouteOptions {

0 commit comments

Comments
 (0)