-
Notifications
You must be signed in to change notification settings - Fork 28
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
base: main
Are you sure you want to change the base?
Conversation
103f6eb
to
3badf6f
Compare
In v19, you need to import from react-dom/client and use the createRoot API.
3badf6f
to
ce70a29
Compare
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. |
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',
},
{ |
In v19, you need to import from react-dom/client and use the createRoot API.