Skip to content
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

Add support for React 18 server rendering #1409

Merged
merged 4 commits into from
Dec 24, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if the @ts-expect-error messages are still needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, these are still needed. We could upgrade the types library to the react beta to get around having to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pros and cons of doing that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the pros are:

  1. Using an API that is up-to-date with the latest React.

The cons are a couple:

  1. It would be depending on beta code for our types
  2. It could potentially have removed deprecated types that are no longer used in React 18. This could be seen as a positive as well because it would alert us to changes in the API that we might be using.

My recommendation would be to ignore it for now until it gets out of beta and then upgrade the types.

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';

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if the @ts-expect-error messages are still needed.

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';

export default function reactRender(domNode: Element, reactElement: ReactElement): void | Element | Component {
// @ts-expect-error potentially present if React 18 or greater
if (ReactDOM.createRoot) {
// @ts-expect-error potentially present if React 18 or greater
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if the @ts-expect-error messages are still needed.

const root = ReactDOM.createRoot(domNode);
root.render(reactElement);
return root
}

// eslint-disable-next-line react/no-render-return-value
return ReactDOM.render(reactElement, domNode);
}