Skip to content

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.

2 participants