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

WIP Add support for newer versions of react-dom #300

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

lencioni
Copy link
Contributor

@lencioni lencioni commented Nov 1, 2024

In v19, you need to import from react-dom/client and use the createRoot API.

In v19, you need to import from react-dom/client and use the createRoot
API.
@lencioni
Copy link
Contributor Author

I'm running into some issues with the tests. It seems that root.render triggers some asynchronous things that may happen later than the previous way of rendering (e.g. componentDidMount or useEffect). The way to wait for these is to wrap things in React's act function, which seems to help a bit (but also causes a bunch of warnings to be logged). I don't see a clear path forward yet, unfortunately. My hunch is that we may need to restructure things a bit in order to get this to work.

@lencioni
Copy link
Contributor Author

I can get the tests to pass with this diff, but I don't think all of the changes I had to make to the test are what we want...

diff --git a/src/browser/processor.js b/src/browser/processor.js
index 19cc933..bd2271a 100644
--- a/src/browser/processor.js
+++ b/src/browser/processor.js
@@ -41,6 +41,7 @@ async function renderExample(exampleRenderFunc, { component, variant }) {
     window.happoRender(renderResult, { rootElement, component, variant });
 
   const result = exampleRenderFunc(renderInDom);
+
   if (result && typeof result.then === 'function') {
     // this is a promise
     await result;
@@ -142,7 +143,8 @@ export default class Processor {
     const { component, fileName, variant, render } =
       this.flattenedExamples[this.cursor];
     const exampleRenderFunc = getRenderFunc(render);
-    window.happoCleanup();
+    await window.happoCleanup();
+
     try {
       window.verbose(`Rendering component ${component}, variant ${variant}`);
       await renderExample(exampleRenderFunc, { component, variant });
@@ -152,11 +154,13 @@ export default class Processor {
         e,
       );
     }
+
     const root =
       (this.rootElementSelector &&
         document.body.querySelector(this.rootElementSelector)) ||
       findRoot();
     const html = await this.waitForHTML(root);
+
     const item = {
       html,
       css: '', // Can we remove this?
@@ -164,10 +168,12 @@ export default class Processor {
       variant,
       assetPaths: findAssetPaths(),
     };
+
     const { stylesheets } = render;
     if (stylesheets) {
       item.stylesheets = stylesheets;
     }
+
     return item;
   }
 
diff --git a/src/createDynamicEntryPoint.js b/src/createDynamicEntryPoint.js
index d49b536..f602f9e 100644
--- a/src/createDynamicEntryPoint.js
+++ b/src/createDynamicEntryPoint.js
@@ -61,18 +61,29 @@ export default async function createDynamicEntryPoint({
     const reactDomMajorVersion = parseInt(reactDomVersion.split('.', 1)[0], 10);
     if (reactDomMajorVersion >= 18) {
       const pathToReactDom = require.resolve('react-dom/client');
+      const pathToReact = require.resolve('react');
       strings.push(
         `
+        global.IS_REACT_ACT_ENVIRONMENT = true;
         const ReactDOM = require('${pathToReactDom}');
+        const { act } = require('${pathToReact}');
         let root;
         window.happoRender = (reactComponent, { rootElement, component, variant }) => {
-          root = ReactDOM.createRoot(rootElement);
-          root.render(renderWrapper(reactComponent, { component, variant }));
+          if (!root) {
+            root = ReactDOM.createRoot(rootElement);
+          }
+
+          act(() => {
+            root.render(renderWrapper(reactComponent, { component, variant }));
+          });
         };
 
         window.happoCleanup = () => {
           if (root) {
-            root.unmount();
+            act(() => {
+              root.unmount();
+            });
+            root = null;
           }
         };
         `.trim(),
diff --git a/test/integrations/examples/Foo-react-happo.js b/test/integrations/examples/Foo-react-happo.js
index f512805..e2bb9a5 100644
--- a/test/integrations/examples/Foo-react-happo.js
+++ b/test/integrations/examples/Foo-react-happo.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { createPortal } from 'react-dom';
+import * as ReactDOM from 'react-dom';
 
 import Button from './Button.ffs';
 import ThemeContext from '../theme';
@@ -22,17 +22,22 @@ export const anotherVariant = () => {
 const PortalComponent = ({ children }) => {
   const element = document.createElement('div');
   document.body.appendChild(element);
-  return createPortal(children, document.body);
+  return ReactDOM.createPortal(children, element);
 };
 
-export const portalExample = () => (
-  <PortalComponent>
-    {window.navigator.userAgent === 'happo-puppeteer'
-      ? 'forbidden'
-      : window.localStorage.getItem('foobar')}
-    <button type="button">I am in a portal</button>
-  </PortalComponent>
-);
+export const portalExample = (renderInDOM) => {
+  renderInDOM(
+    <PortalComponent>
+      {window.navigator.userAgent === 'happo-puppeteer'
+        ? 'forbidden'
+        : window.localStorage.getItem('foobar')}
+      <button type="button">I am in a portal</button>
+    </PortalComponent>,
+  );
+  return new Promise((resolve) => {
+    setTimeout(resolve, 10);
+  });
+};
 
 export const innerPortal = () => (
   <>
@@ -45,6 +50,7 @@ class AsyncComponent extends React.Component {
   constructor(props) {
     super(props);
     this.state = {
+      ready: false,
       label: 'Not ready',
     };
     this.setLabel = this.setLabel.bind(this);
@@ -76,7 +82,9 @@ class AsyncComponent extends React.Component {
 
 export const asyncExample = (render) => {
   render(<AsyncComponent />);
-  window.dispatchEvent(new CustomEvent('set-label', { detail: 'Ready' }));
+  React.act(() => {
+    window.dispatchEvent(new CustomEvent('set-label', { detail: 'Ready' }));
+  });
   return new Promise((resolve) => {
     setTimeout(resolve, 11);
   });
@@ -92,7 +100,9 @@ export const emptyForever = () => <EmptyComponent />;
 class DynamicImportExample extends React.Component {
   constructor(props) {
     super(props);
-    this.state = {};
+    this.state = {
+      text: 'Loading...',
+    };
   }
 
   async componentDidMount() {
@@ -106,7 +116,12 @@ class DynamicImportExample extends React.Component {
   }
 }
 
-export const dynamicImportExample = () => <DynamicImportExample />;
+export const dynamicImportExample = (renderInDOM) => {
+  renderInDOM(<DynamicImportExample />);
+  return new Promise((resolve) => {
+    setTimeout(resolve, 10);
+  });
+};
 
 export const themedExample = () => (
   <ThemeContext.Consumer>
diff --git a/test/integrations/react-test.js b/test/integrations/react-test.js
index 4974e4e..2fdaa63 100644
--- a/test/integrations/react-test.js
+++ b/test/integrations/react-test.js
@@ -224,7 +224,7 @@ it('produces the right html', async () => {
     {
       component: 'Foo-react',
       css: '',
-      html: '<button type="button"></button>',
+      html: '<button type="button">Not ready</button>',
       variant: 'asyncWithoutPromise',
     },
     {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant