-
Notifications
You must be signed in to change notification settings - Fork 951
Upgrade React from 17 to 19 #10295
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
Open
ranm8
wants to merge
3
commits into
master
Choose a base branch
from
ui-react-19-upgrade
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Upgrade React from 17 to 19 #10295
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { Request } from 'express'; | ||
| import { BrowserData, PartialLocation } from './request-browser'; | ||
|
|
||
| /** extract BrowserData from Express request (convinience method) */ | ||
| export function browserFromExpress(req: Request, port: number): BrowserData { | ||
| return { | ||
| location: requestToLocation(req, port), | ||
| headers: req.headers, | ||
| }; | ||
| } | ||
|
|
||
| function requestToLocation(request: Request, port: number): PartialLocation { | ||
| return { | ||
| host: `${request.hostname}:${port}`, | ||
| hostname: request.hostname, | ||
| href: `${request.protocol}://${request.hostname}:${port}${request.url}`, | ||
| origin: `${request.protocol}://${request.hostname}:${port}`, | ||
| pathname: request.path, | ||
| port: port.toString(), | ||
| protocol: `${request.protocol}:`, | ||
|
|
||
| hash: '', | ||
| search: extractSearch(request.url), | ||
| }; | ||
| } | ||
|
|
||
| function extractSearch(url: string) { | ||
| const [, after] = url.split('?'); | ||
| if (!after) return ''; | ||
|
|
||
| return `?${after}`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import React, { type ReactNode } from 'react'; | ||
| import { Html, mountPointId, ssrCleanup } from '@teambit/ui-foundation.ui.rendering.html'; | ||
| import { Composer, type Wrapper } from '@teambit/base-ui.utils.composer'; | ||
| import ReactDOM from 'react-dom'; | ||
| import compact from 'lodash.compact'; | ||
| import type { ClientRenderPlugin } from './render-plugins'; | ||
|
|
||
| type ReactRoot = { render: (children: ReactNode) => void }; | ||
|
|
||
| /** | ||
| * creates a React root using createRoot (React 18+) when available, | ||
| * falling back to ReactDOM.render (React 17) otherwise. | ||
| */ | ||
| function createReactRoot(container: HTMLElement): ReactRoot { | ||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const { createRoot } = require('react-dom/client'); | ||
| return createRoot(container); | ||
| } catch { | ||
| return { | ||
| render: (children: ReactNode) => { | ||
| // @ts-ignore | ||
| ReactDOM.render(children as React.ReactElement, container); | ||
| }, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| export type BrowserRendererOptions = { | ||
| /** load and remove dehydrated state from the dom */ | ||
| popAssets: () => Map<string, string>; | ||
| /** mount point element or id */ | ||
| mountPointElement: string | HTMLElement; | ||
| /** runs after rehydration */ | ||
| cleanup: () => void; | ||
| }; | ||
| const defaultOptions: BrowserRendererOptions = { | ||
| popAssets: Html.popAssets, | ||
| mountPointElement: mountPointId, | ||
| cleanup: ssrCleanup, | ||
| }; | ||
|
|
||
| export class BrowserRenderer { | ||
| options: BrowserRendererOptions; | ||
| private root: ReactRoot | undefined; | ||
| constructor( | ||
| /** effect rendering at key triggers. keep order consistent between server and browser */ | ||
| private plugins: ClientRenderPlugin<any, any>[], | ||
| options?: Partial<BrowserRendererOptions> | ||
| ) { | ||
| this.options = { ...options, ...defaultOptions }; | ||
| } | ||
|
|
||
| /** render and rehydrate client-side */ | ||
| async render(children: ReactNode) { | ||
| // (*) load state from the dom | ||
| const deserializedState = await this.deserialize(); | ||
|
|
||
| // (1) init setup client plugins | ||
| let renderContexts = await this.triggerBrowserInit(deserializedState); | ||
|
|
||
| // (2) make react dom | ||
| const reactContexts = this.getReactContexts(renderContexts); | ||
| const app = <Composer components={reactContexts}>{children}</Composer>; | ||
|
|
||
| renderContexts = await this.triggerBeforeHydrateHook(renderContexts, app); | ||
|
|
||
| // (3) render / rehydrate | ||
| const mountPoint = | ||
| typeof this.options.mountPointElement === 'string' | ||
| ? document.getElementById(this.options.mountPointElement) | ||
| : this.options.mountPointElement; | ||
| if (!this.root && mountPoint) { | ||
| this.root = createReactRoot(mountPoint); | ||
| } | ||
| this.root?.render(app); | ||
|
|
||
| await this.triggerHydrateHook(renderContexts, mountPoint); | ||
|
|
||
| // (3.1) remove ssr only styles | ||
| this.options.cleanup(); | ||
| } | ||
|
|
||
| private async deserialize() { | ||
| const { plugins } = this; | ||
| const rawAssets = this.options.popAssets(); | ||
|
|
||
| const deserialized = await Promise.all( | ||
| plugins.map(async (plugin) => { | ||
| if (!('deserialize' in plugin)) return undefined; | ||
| if (!plugin.key) throw new Error('Key is required for .deserialize()'); | ||
|
|
||
| try { | ||
| const raw = rawAssets.get(plugin.key); | ||
| return plugin.deserialize?.(raw); | ||
| } catch (e) { | ||
| // eslint-disable-next-line no-console | ||
| console.error(`failed deserializing server state for aspect "${plugin.key}"`, e); | ||
| return undefined; | ||
| } | ||
| }) | ||
| ); | ||
|
|
||
| return deserialized; | ||
| } | ||
|
|
||
| private triggerBrowserInit(deserializedState: any[]) { | ||
| const { plugins } = this; | ||
|
|
||
| const initPromises = plugins.map((plugin, idx) => { | ||
| const state = deserializedState[idx]; | ||
| return plugin.browserInit?.(state); | ||
| }); | ||
| return Promise.all(initPromises); | ||
| } | ||
|
|
||
| private triggerBeforeHydrateHook(renderContexts: any[], app: JSX.Element) { | ||
| const { plugins } = this; | ||
|
|
||
| const promises = plugins.map(async (plugin, idx) => { | ||
| const ctx = renderContexts[idx]; | ||
| const nextCtx = await plugin.onBeforeHydrate?.(ctx, app); | ||
| return nextCtx || ctx; | ||
| }); | ||
|
|
||
| return Promise.all(promises); | ||
| } | ||
|
|
||
| private async triggerHydrateHook(renderContexts: any[], mountPoint: HTMLElement | null) { | ||
| const { plugins } = this; | ||
|
|
||
| const promises = plugins.map((plugin, idx) => { | ||
| const renderCtx = renderContexts[idx]; | ||
| return plugin.onHydrate?.(renderCtx, mountPoint); | ||
| }); | ||
|
|
||
| await Promise.all(promises); | ||
| } | ||
|
|
||
| private getReactContexts(renderContexts: any[]): Wrapper[] { | ||
| const { plugins } = this; | ||
|
|
||
| return compact( | ||
| plugins.map((plugin, idx) => { | ||
| const renderCtx = renderContexts[idx]; | ||
| const props = { renderCtx }; | ||
| const decorator = plugin.reactClientContext || plugin.reactContext; | ||
| if (!decorator) return undefined; | ||
| return [decorator, props]; | ||
| }) | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export type { Assets as HtmlAssets } from '@teambit/ui-foundation.ui.rendering.html'; | ||
| export { browserFromExpress } from './browser-from-express'; | ||
| export { BrowserRenderer } from './browser-renderer'; | ||
| export { Ssr } from './react-ssr'; | ||
| export type { ReactSsrOptions } from './react-ssr'; | ||
| export type { ClientRenderPlugin, ContextProps, RenderPlugin, ServerRenderPlugin } from './render-plugins'; | ||
| export type { BrowserData, PartialLocation } from './request-browser'; | ||
| export { ServerRenderer } from './server-renderer'; | ||
| export type { SsrSession } from './ssr-session'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| --- | ||
| description: A complete and extendable server-side rendering solution. | ||
| labels: ['react', 'serverside rendering', 'ssr'] | ||
| --- | ||
|
|
||
| Break down Server-side Rendering (SSR) into simple plugins. | ||
|
|
||
| React version: `17.x.x` | ||
|
|
||
| ## Simple usage | ||
|
|
||
| Out of the box, SSR is very simple: | ||
|
|
||
| ```tsx | ||
| // client/main.js | ||
| import { React } from 'react'; | ||
| import { BrowserRenderer } from '@teambit/react.rendering.ssr'; | ||
|
|
||
| const renderer = new BrowserRenderer([]); | ||
| renderer.render(<MyApp />); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // server/main.js | ||
| import { React } from 'react'; | ||
| import { ServerRenderer } from '@teambit/react.rendering.ssr'; | ||
|
|
||
| const renderer = new ServerRenderer([]); | ||
|
|
||
| // can be async | ||
| export default async function ServerSideApp({ response }) { | ||
| const content = await renderer.render(<MyApp />); | ||
| response.send(content); | ||
| } | ||
| ``` | ||
|
|
||
| and that's it! pretty simple. | ||
|
|
||
| ### React Router Dom example | ||
|
|
||
| While rendering, you will have all the context you will need: | ||
|
|
||
| ```tsx | ||
| // client/main.js | ||
| import { React } from 'react'; | ||
| import { BrowserRenderer } from '@teambit/react.rendering.ssr'; | ||
|
|
||
| const renderer = new BrowserRenderer([]); | ||
| renderer.render( | ||
| <BrowserRouter> | ||
| <MyApp /> | ||
| </BrowserRouter> | ||
| ); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // server/main.js | ||
| import { React } from 'react'; | ||
| import { ServerRenderer } from '@teambit/react.rendering.ssr'; | ||
|
|
||
| const renderer = new ServerRenderer([]); | ||
|
|
||
| export default async function ServerSideApp({ response, browser }) { | ||
| const content = await renderer.render( | ||
| <StaticRouter location={browser.location}> | ||
| <MyApp /> | ||
| </StaticRouter> | ||
| ); | ||
| response.send(content); | ||
| } | ||
| ``` | ||
|
|
||
| ## Plugins | ||
|
|
||
| Instead of manually writing different code for server and client, you can write a single or double plugin that will manage the entire ssr lifecycle: | ||
|
|
||
| > This is the _unified_ plugin. You can also use the `ClientRenderPlugin` and `ServerRenderPlugin` separately. | ||
|
|
||
| ```tsx | ||
| class UnifiedReactRouterPlugin implements RenderPlugin<{ location: PartialLocation }> { | ||
| serverInit = ({ browser }: SsrSession) => ({ location: browser.location }); | ||
| // not needed in this case | ||
| // clientInit = () => { return { location: window.location }} | ||
|
|
||
| reactServerContext = ({ children, renderCtx }) => { | ||
| return <StaticRouter location={renderCtx.location}>{children}</StaticRouter>; | ||
| }; | ||
|
|
||
| // needed for serialization | ||
| key: 'react-router-ssr-plugin'; | ||
|
|
||
| serialize = (ctx, app) => { | ||
| // ... send data to the client | ||
| }; | ||
|
|
||
| deserialize = (rawData) => { | ||
| // ... parse stored data | ||
| }; | ||
|
|
||
| reactClientContext = ({ children }) => { | ||
| return <BrowserRouter>{children}</BrowserRouter>; | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| You can then use the plugin like so: | ||
|
|
||
| ```tsx | ||
| // client | ||
| const renderer = new BrowserRenderer([myPlugin]); | ||
|
|
||
| // server | ||
| const renderer = new ServerRenderer([myPlugin]); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import type { RenderPlugin } from './render-plugins'; | ||
| import { BrowserRenderer } from './browser-renderer'; | ||
| import type { BrowserRendererOptions } from './browser-renderer'; | ||
| import { ServerRenderer } from './server-renderer'; | ||
| import type { ServerRendererOptions } from './server-renderer'; | ||
|
|
||
| export type ReactSsrOptions = Partial<ServerRendererOptions & BrowserRendererOptions>; | ||
|
|
||
| export class Ssr { | ||
| constructor( | ||
| // create array once, to keep consistent indexes between server and client | ||
| private plugins: RenderPlugin<any, any>[], | ||
| private options?: ReactSsrOptions | ||
| ) {} | ||
| private browser = new BrowserRenderer(this.plugins, this.options); | ||
| private server = new ServerRenderer(this.plugins, this.options); | ||
|
|
||
| renderServer = this.server.render.bind(this.server); | ||
| renderBrowser = this.browser.render.bind(this.browser); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.