From 68019ae1854773c776d756e729fb9a125ee3b66d Mon Sep 17 00:00:00 2001 From: Alexey Romanov Date: Wed, 22 Jun 2022 19:41:04 +0300 Subject: [PATCH] Require createRoot/hydrateRoot from 'react-dom/client' under react-dom 18+ Fixes #1441 --- node_package/src/ReactOnRails.ts | 7 +-- node_package/src/clientStartup.ts | 7 +-- node_package/src/reactHydrate.ts | 12 ----- node_package/src/reactHydrateOrRender.ts | 41 +++++++++++++++++ node_package/src/reactRender.ts | 15 ------- node_package/src/supportsReactCreateRoot.ts | 8 ---- .../tests/supportsReactCreateRoot.test.js | 44 ------------------- package.json | 2 +- spec/dummy/yarn.lock | 11 +++-- yarn.lock | 10 ++--- 10 files changed, 56 insertions(+), 101 deletions(-) delete mode 100644 node_package/src/reactHydrate.ts create mode 100644 node_package/src/reactHydrateOrRender.ts delete mode 100644 node_package/src/reactRender.ts delete mode 100644 node_package/src/supportsReactCreateRoot.ts delete mode 100644 node_package/tests/supportsReactCreateRoot.test.js diff --git a/node_package/src/ReactOnRails.ts b/node_package/src/ReactOnRails.ts index 16ab9efbf1..8d5375ddfb 100644 --- a/node_package/src/ReactOnRails.ts +++ b/node_package/src/ReactOnRails.ts @@ -18,8 +18,7 @@ import type { AuthenticityHeaders, StoreGenerator } from './types/index'; -import reactHydrate from './reactHydrate'; -import reactRender from './reactRender'; +import reactHydrateOrRender from './reactHydrateOrRender'; /* eslint-disable @typescript-eslint/no-explicit-any */ type Store = any; @@ -188,9 +187,7 @@ ctx.ReactOnRails = { const componentObj = ComponentRegistry.get(name); const reactElement = createReactOutput({ componentObj, props, domNodeId }); - const render = hydrate ? reactHydrate : reactRender; - // eslint-disable-next-line react/no-render-return-value - return render(document.getElementById(domNodeId) as Element, reactElement as ReactElement); + return reactHydrateOrRender(hydrate, document.getElementById(domNodeId) as Element, reactElement as ReactElement); }, /** diff --git a/node_package/src/clientStartup.ts b/node_package/src/clientStartup.ts index 024dc9ff8d..b38755c706 100644 --- a/node_package/src/clientStartup.ts +++ b/node_package/src/clientStartup.ts @@ -9,8 +9,7 @@ import type { import createReactOutput from './createReactOutput'; import {isServerRenderHash} from './isServerRenderResult'; -import reactHydrate from './reactHydrate'; -import reactRender from './reactRender'; +import reactHydrateOrRender from './reactHydrateOrRender'; declare global { interface Window { @@ -168,10 +167,8 @@ function render(el: Element, railsContext: RailsContext): void { throw new Error(`\ 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) { - reactHydrate(domNode, reactElementOrRouterResult as ReactElement); } else { - reactRender(domNode, reactElementOrRouterResult as ReactElement); + reactHydrateOrRender(shouldHydrate, domNode, reactElementOrRouterResult as ReactElement); } } } catch (e: any) { diff --git a/node_package/src/reactHydrate.ts b/node_package/src/reactHydrate.ts deleted file mode 100644 index 5b92b9ca2c..0000000000 --- a/node_package/src/reactHydrate.ts +++ /dev/null @@ -1,12 +0,0 @@ -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); -} diff --git a/node_package/src/reactHydrateOrRender.ts b/node_package/src/reactHydrateOrRender.ts new file mode 100644 index 0000000000..f068c755e1 --- /dev/null +++ b/node_package/src/reactHydrateOrRender.ts @@ -0,0 +1,41 @@ +import { ReactElement, Component } from 'react'; +import ReactDOM from 'react-dom'; + +type HydrateOrRenderReturnType = void | Element | Component; +type HydrateOrRenderType = (domNode: Element, reactElement: ReactElement) => HydrateOrRenderReturnType; +const supportsReactCreateRoot = parseInt(ReactDOM.version.split('.')[0], 10) >= 18; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let reactDomClient: any; +if (supportsReactCreateRoot) { + if (require.context) { + // we are in a Webpack environment (and in a node-modules directory of another project) + // See https://webpack.js.org/guides/dependency-management/#context-module-api + const reactDomContext = require.context('../react-dom/', false, /^client\.js$/); + reactDomClient = reactDomContext('../react-dom/client.js'); + } else { + // we want a dynamic require here so that webpack doesn't rewrite it + const reactDomClientName = 'react-dom/client'; + // eslint-disable-next-line global-require,import/no-dynamic-require + reactDomClient = require(reactDomClientName); + } +} + +export const reactHydrate: HydrateOrRenderType = supportsReactCreateRoot ? + reactDomClient.hydrateRoot : + (domNode, reactElement) => ReactDOM.hydrate(reactElement, domNode); + +export function reactRender(domNode: Element, reactElement: ReactElement): HydrateOrRenderReturnType { + if (supportsReactCreateRoot) { + const root = reactDomClient.createRoot(domNode); + root.render(reactElement); + return root; + } + + // eslint-disable-next-line react/no-render-return-value + return ReactDOM.render(reactElement, domNode); +} + +export default function reactHydrateOrRender(shouldHydrate: boolean, domNode: Element, reactElement: ReactElement): HydrateOrRenderReturnType { + return shouldHydrate ? reactHydrate(domNode, reactElement) : reactRender(domNode, reactElement); +} diff --git a/node_package/src/reactRender.ts b/node_package/src/reactRender.ts deleted file mode 100644 index 36a57c3303..0000000000 --- a/node_package/src/reactRender.ts +++ /dev/null @@ -1,15 +0,0 @@ -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); -} diff --git a/node_package/src/supportsReactCreateRoot.ts b/node_package/src/supportsReactCreateRoot.ts deleted file mode 100644 index 3ea4e9e6cc..0000000000 --- a/node_package/src/supportsReactCreateRoot.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ReactDOM from 'react-dom'; - -export const isVersionGreaterThanOrEqualTo18 = (): boolean => ( - ReactDOM.version && parseInt(ReactDOM.version.split('.')[0], 10) >= 18 || - false -) - -export default isVersionGreaterThanOrEqualTo18(); diff --git a/node_package/tests/supportsReactCreateRoot.test.js b/node_package/tests/supportsReactCreateRoot.test.js deleted file mode 100644 index 44f521f941..0000000000 --- a/node_package/tests/supportsReactCreateRoot.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import ReactDOM from 'react-dom'; -import { isVersionGreaterThanOrEqualTo18 } from '../src/supportsReactCreateRoot'; - -describe('supportsReactCreateRoot', () => { - it('returns false for ReactDOM v16, no version', () => { - expect.assertions(1); - const originalValue = ReactDOM.version; - delete ReactDOM.version; - expect(isVersionGreaterThanOrEqualTo18()).toBe(false); - ReactDOM.version = originalValue; - }); - - it('returns false for ReactDOM v17', () => { - const originalValue = ReactDOM.version; - ReactDOM.version = '17.0.0'; - expect.assertions(1); - expect(isVersionGreaterThanOrEqualTo18()).toBe(false); - ReactDOM.version = originalValue; - }); - - it('returns true for ReactDOM v18', () => { - expect.assertions(1); - const originalValue = ReactDOM.version; - ReactDOM.version = '18.0.0'; - expect(isVersionGreaterThanOrEqualTo18()).toBe(true); - ReactDOM.version = originalValue; - }); - - it('returns true for ReactDOM v19', () => { - expect.assertions(1); - const originalValue = ReactDOM.version; - ReactDOM.version = '19.0.0'; - expect(isVersionGreaterThanOrEqualTo18()).toBe(true); - ReactDOM.version = originalValue; - }); - - it('returns true for ReactDOM v18 beta', () => { - expect.assertions(1); - const originalValue = ReactDOM.version; - ReactDOM.version = '18.0.0-rc.2'; - expect(isVersionGreaterThanOrEqualTo18()).toBe(true); - ReactDOM.version = originalValue; - }); -}); diff --git a/package.json b/package.json index bee1c96219..e700ac0fbe 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "@babel/preset-react": "^7.12.10", "@babel/types": "^7.12.10", "@types/jest": "^26.0.18", - "@types/node": "^14.14.11", "@types/react": "^16.9.23", "@types/react-dom": "^16.9.5", "@types/turbolinks": "^5.2.0", + "@types/webpack-env": "^1.17.0", "@typescript-eslint/eslint-plugin": "^4.10.0", "@typescript-eslint/parser": "^4.10.0", "babelify": "^10.0.0", diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 69ab4f25ac..612637c550 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -6349,7 +6349,7 @@ react-is@^16.7.0: integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== "react-on-rails@file:.yalc/react-on-rails": - version "13.0.1" + version "13.0.2" dependencies: "@babel/runtime-corejs3" "^7.12.5" concurrently "^5.1.0" @@ -6983,11 +6983,10 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shakapacker@6.4.0: - name shakapacker - version "6.4.0" - resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-6.4.0.tgz#f49ad8bb51eb1a151b190b588ad467c6e6f62e69" - integrity sha512-bxxi7Lla3n2fnFjxVv6BtD2vcTEZIm2eb29WkJqZBmpFVT9tnwbd3XQISD9eOst6kTEJu2ruUp7dGvuEjpxwkg== +shakapacker@^6.4.0: + version "6.4.1" + resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-6.4.1.tgz#4c3662c63acf54ecd7847f97ceb88ee1a23bfa2a" + integrity sha512-yCGqDVNP5Dz4W8wAl53M/zG6/xpvAaFItPVVlDuHyq3xCzHD2MYEBvmITNgBvvl2WU34OPcEIeVrXOWcl/MCMQ== dependencies: glob "^7.2.0" js-yaml "^4.1.0" diff --git a/yarn.lock b/yarn.lock index b99522a898..276ccb7df7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,11 +1594,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313" integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== -"@types/node@^14.14.11": - version "14.14.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.11.tgz#fc25a4248a5e8d0837019b1d170146d07334abe0" - integrity sha512-BJ97wAUuU3NUiUCp44xzUFquQEvnk1wu7q4CMEUYKJWjdkr0YWYDsm4RFtAvxYsNjLsKcrFt6RvK8r+mnzMbEQ== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -1639,6 +1634,11 @@ resolved "https://registry.yarnpkg.com/@types/turbolinks/-/turbolinks-5.2.0.tgz#cdfd3691143ea2f452113c2a06bd12d9a88b25d6" integrity sha512-xEgHb25lj23EaUlsU3Y4KlFH7elhlYINGSnqyn/8zmcMnenY4Idyjk/x9kw1kOoOToY3de9fD8NSwRzNc6f5nw== +"@types/webpack-env@^1.17.0": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" + integrity sha512-eHSaNYEyxRA5IAG0Ym/yCyf86niZUIF/TpWKofQI/CVfh5HsMEUyfE2kwFxha4ow0s5g0LfISQxpDKjbRDrizw== + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"