Skip to content

Commit

Permalink
Add support for React 18 server rendering (#1409)
Browse files Browse the repository at this point in the history
* add support diff versions of React's render API

in React 18, the API for rendering and hydrating elements is updated.  

This updates the syntax to support both syntaxes so that users of both APIs can still work with the same interface.

* move files to reusables so that they can work for ReactOnRails
* use ReactDOM.version to determine API for rendering and hydrating
  • Loading branch information
kylemellander authored Dec 24, 2021
1 parent 64f722f commit 922e19c
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 6 deletions.
7 changes: 4 additions & 3 deletions node_package/src/ReactOnRails.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ReactDOM from 'react-dom';
import type { ReactElement, Component } from 'react';

import * as ClientStartup from './clientStartup';
Expand All @@ -19,6 +18,8 @@ import type {
AuthenticityHeaders,
StoreGenerator
} from './types/index';
import reactHydrate from './reactHydrate';
import reactRender from './reactRender';

/* eslint-disable @typescript-eslint/no-explicit-any */
type Store = any;
Expand Down Expand Up @@ -187,9 +188,9 @@ ctx.ReactOnRails = {
const componentObj = ComponentRegistry.get(name);
const reactElement = createReactOutput({ componentObj, props, domNodeId });

const render = hydrate ? ReactDOM.hydrate : ReactDOM.render;
const render = hydrate ? reactHydrate : reactRender;
// eslint-disable-next-line react/no-render-return-value
return render(reactElement as ReactElement, document.getElementById(domNodeId));
return render(document.getElementById(domNodeId) as Element, reactElement as ReactElement);
},

/**
Expand Down
9 changes: 6 additions & 3 deletions node_package/src/clientStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type {

import createReactOutput from './createReactOutput';
import {isServerRenderHash} from './isServerRenderResult';
import reactHydrate from './reactHydrate';
import reactRender from './reactRender';

declare global {
interface Window {
Expand Down Expand Up @@ -150,7 +152,8 @@ function render(el: Element, railsContext: RailsContext): void {
}

// Hydrate if available and was server rendered
const shouldHydrate = !!ReactDOM.hydrate && !!domNode.innerHTML;
// @ts-expect-error potentially present if React 18 or greater
const shouldHydrate = !!(ReactDOM.hydrate || ReactDOM.hydrateRoot) && !!domNode.innerHTML;

const reactElementOrRouterResult = createReactOutput({
componentObj,
Expand All @@ -166,9 +169,9 @@ function render(el: Element, railsContext: RailsContext): void {
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
You should return a React.Component always for the client side entry point.`);
} else if (shouldHydrate) {
ReactDOM.hydrate(reactElementOrRouterResult as ReactElement, domNode);
reactHydrate(domNode, reactElementOrRouterResult as ReactElement);
} else {
ReactDOM.render(reactElementOrRouterResult as ReactElement, domNode);
reactRender(domNode, reactElementOrRouterResult as ReactElement);
}
}
} catch (e) {
Expand Down
12 changes: 12 additions & 0 deletions node_package/src/reactHydrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ReactDOM from 'react-dom';
import { ReactElement, Component } from 'react';
import supportsReactCreateRoot from './supportsReactCreateRoot';

export default function reactHydrate(domNode: Element, reactElement: ReactElement): void | Element | Component {
if (supportsReactCreateRoot) {
// @ts-expect-error potentially present if React 18 or greater
return ReactDOM.hydrateRoot(domNode, reactElement);
}

return ReactDOM.hydrate(reactElement, domNode);
}
15 changes: 15 additions & 0 deletions node_package/src/reactRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ReactDOM from 'react-dom';
import { ReactElement, Component } from 'react';
import supportsReactCreateRoot from './supportsReactCreateRoot';

export default function reactRender(domNode: Element, reactElement: ReactElement): void | Element | Component {
if (supportsReactCreateRoot) {
// @ts-expect-error potentially present if React 18 or greater
const root = ReactDOM.createRoot(domNode);
root.render(reactElement);
return root
}

// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(reactElement, domNode);
}
4 changes: 4 additions & 0 deletions node_package/src/supportsReactCreateRoot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ReactDOM from 'react-dom';

const reactMajorVersion = parseInt(ReactDOM.version.split('.')[0], 10);
export default reactMajorVersion >= 18;

0 comments on commit 922e19c

Please sign in to comment.