Skip to content

Commit

Permalink
BUGFIX: Replace deprecated ReactDOM.render with createRoot (#25)
Browse files Browse the repository at this point in the history
This PR addresses warnings appearing on the console by updating
occurrences of the deprecated `ReactDOM.render` function to
`createRoot`, as necessitated by newer versions of React (>18).

To ensure proper order of mounting and unmounting new roots and to avoid
potential race conditions, a workaround utilizing `setTimeout` with a
time of 0 was implemented. This workaround synchronizes the mount and
unmount operations, aligning with the asynchronous behaviour of
`createRoot`.
  • Loading branch information
aneuwald-ctw authored May 8, 2024
1 parent d3619ad commit c929670
Show file tree
Hide file tree
Showing 16 changed files with 124 additions and 79 deletions.
6 changes: 3 additions & 3 deletions benchmark/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";

import Logger from "@foxglove/log";
import { initI18n } from "@foxglove/studio-base";
Expand All @@ -29,8 +29,8 @@ async function main() {

const { Root } = await import("./Root");

// eslint-disable-next-line react/no-deprecated
ReactDOM.render(<Root />, rootEl);
const root = createRoot(rootEl!);
root.render(<Root />);
}

void main();
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "foxbox",
"version": "1.0.0",
"version": "1.0.1",
"license": "MPL-2.0",
"private": true,
"productName": "Foxbox",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// You may not use this file except in compliance with the License.

import { SnackbarProvider } from "notistack";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { act } from "react-dom/test-utils";

import DocumentDropListener from "@foxglove/studio-base/components/DocumentDropListener";
Expand All @@ -30,17 +30,18 @@ describe("<DocumentDropListener>", () => {
wrapper = document.createElement("div");
document.body.appendChild(wrapper);

// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
const root = createRoot(wrapper!);

root.render(
<div>
<SnackbarProvider>
<ThemeProvider isDark={false}>
<DocumentDropListener allowedExtensions={[]} />
</ThemeProvider>
</SnackbarProvider>
</div>,
wrapper,
);

(console.error as jest.Mock).mockClear();
});

Expand All @@ -61,14 +62,14 @@ describe("<DocumentDropListener>", () => {
(event as any).dataTransfer = {
types: ["Files"],
};
act(() => {
document.dispatchEvent(event); // The event should NOT bubble up from the document to the window
});

document.dispatchEvent(event); // The event should NOT bubble up from the document to the window

expect(windowDragoverHandler).not.toHaveBeenCalled();
});

afterEach(() => {
document.body.removeChild(wrapper);
wrapper.remove();
window.removeEventListener("dragover", windowDragoverHandler);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { StoryObj } from "@storybook/react";
import { ReactElement, useLayoutEffect, useState } from "react";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";

import { toSec } from "@foxglove/rostime";
import {
Expand Down Expand Up @@ -94,8 +94,8 @@ function SimplePanel({ context }: { context: PanelExtensionContext }) {
export const SimplePanelRender: StoryObj = {
render: (): ReactElement => {
function initPanel(context: PanelExtensionContext) {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(<SimplePanel context={context} />, context.panelElement);
const root = createRoot(context.panelElement);
root.render(<SimplePanel context={context} />);
}

return (
Expand Down
9 changes: 2 additions & 7 deletions packages/studio-base/src/panels/CallService/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,27 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { StrictMode, useMemo } from "react";
import ReactDOM from "react-dom";

import { useCrash } from "@foxglove/hooks";
import { PanelExtensionContext } from "@foxglove/studio";
import { CaptureErrorBoundary } from "@foxglove/studio-base/components/CaptureErrorBoundary";
import Panel from "@foxglove/studio-base/components/Panel";
import { PanelExtensionAdapter } from "@foxglove/studio-base/components/PanelExtensionAdapter";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";
import { SaveConfig } from "@foxglove/studio-base/types/panels";

import { CallService } from "./CallService";
import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<CallService context={context} />
</CaptureErrorBoundary>
</StrictMode>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}

type Props = {
Expand Down
9 changes: 2 additions & 7 deletions packages/studio-base/src/panels/Gauge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { StrictMode, useMemo } from "react";
import ReactDOM from "react-dom";

import { useCrash } from "@foxglove/hooks";
import { PanelExtensionContext } from "@foxglove/studio";
import { CaptureErrorBoundary } from "@foxglove/studio-base/components/CaptureErrorBoundary";
import Panel from "@foxglove/studio-base/components/Panel";
import { PanelExtensionAdapter } from "@foxglove/studio-base/components/PanelExtensionAdapter";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";
import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider";
import { SaveConfig } from "@foxglove/studio-base/types/panels";

import { Gauge } from "./Gauge";
import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
Expand All @@ -28,10 +27,6 @@ function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionCo
</StrictMode>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}

type Props = {
Expand Down
9 changes: 2 additions & 7 deletions packages/studio-base/src/panels/Indicator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { StrictMode, useMemo } from "react";
import ReactDOM from "react-dom";

import { useCrash } from "@foxglove/hooks";
import { PanelExtensionContext } from "@foxglove/studio";
import { CaptureErrorBoundary } from "@foxglove/studio-base/components/CaptureErrorBoundary";
import Panel from "@foxglove/studio-base/components/Panel";
import { PanelExtensionAdapter } from "@foxglove/studio-base/components/PanelExtensionAdapter";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";
import ThemeProvider from "@foxglove/studio-base/theme/ThemeProvider";
import { SaveConfig } from "@foxglove/studio-base/types/panels";

import { Indicator } from "./Indicator";
import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
Expand All @@ -28,10 +27,6 @@ function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionCo
</StrictMode>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}

type Props = {
Expand Down
18 changes: 5 additions & 13 deletions packages/studio-base/src/panels/Map/initPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import L from "leaflet";
import LeafletRetinaIconUrl from "leaflet/dist/images/marker-icon-2x.png";
import LeafletIconUrl from "leaflet/dist/images/marker-icon.png";
import LeafletShadowIconUrl from "leaflet/dist/images/marker-shadow.png";
import { StrictMode } from "react";
import ReactDOM from "react-dom";

import { useCrash } from "@foxglove/hooks";
import { PanelExtensionContext } from "@foxglove/studio";
import { CaptureErrorBoundary } from "@foxglove/studio-base/components/CaptureErrorBoundary";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";

import MapPanel from "./MapPanel";

Expand All @@ -34,17 +33,10 @@ export function initPanel(
crash: ReturnType<typeof useCrash>,
context: PanelExtensionContext,
): () => void {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<MapPanel context={context} />
</CaptureErrorBoundary>
</StrictMode>,
return createSyncRoot(
<CaptureErrorBoundary onError={crash}>
<MapPanel context={context} />
</CaptureErrorBoundary>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}
9 changes: 2 additions & 7 deletions packages/studio-base/src/panels/Teleop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,26 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { StrictMode, useMemo } from "react";
import ReactDOM from "react-dom";

import { useCrash } from "@foxglove/hooks";
import { PanelExtensionContext } from "@foxglove/studio";
import { CaptureErrorBoundary } from "@foxglove/studio-base/components/CaptureErrorBoundary";
import Panel from "@foxglove/studio-base/components/Panel";
import { PanelExtensionAdapter } from "@foxglove/studio-base/components/PanelExtensionAdapter";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";
import { SaveConfig } from "@foxglove/studio-base/types/panels";

import TeleopPanel from "./TeleopPanel";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<TeleopPanel context={context} />
</CaptureErrorBoundary>
</StrictMode>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}

type Props = {
Expand Down
9 changes: 2 additions & 7 deletions packages/studio-base/src/panels/ThreeDeeRender/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { StrictMode, useMemo } from "react";
import ReactDOM from "react-dom";
import { DeepPartial } from "ts-essentials";

import { useCrash } from "@foxglove/hooks";
Expand All @@ -20,6 +19,7 @@ import {
} from "@foxglove/studio-base/components/PanelExtensionAdapter";
import { INJECTED_FEATURE_KEYS, useAppContext } from "@foxglove/studio-base/context/AppContext";
import { TestOptions } from "@foxglove/studio-base/panels/ThreeDeeRender/IRenderer";
import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";
import { SaveConfig } from "@foxglove/studio-base/types/panels";

import { SceneExtensionConfig } from "./SceneExtensionConfig";
Expand All @@ -36,8 +36,7 @@ type InitPanelArgs = {

function initPanel(args: InitPanelArgs, context: BuiltinPanelExtensionContext) {
const { crash, forwardedAnalytics, interfaceMode, testOptions, customSceneExtensions } = args;
// eslint-disable-next-line react/no-deprecated
ReactDOM.render(
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ForwardAnalyticsContextProvider forwardedAnalytics={forwardedAnalytics}>
Expand All @@ -52,10 +51,6 @@ function initPanel(args: InitPanelArgs, context: BuiltinPanelExtensionContext) {
</StrictMode>,
context.panelElement,
);
return () => {
// eslint-disable-next-line react/no-deprecated
ReactDOM.unmountComponentAtNode(context.panelElement);
};
}

type Props = {
Expand Down
39 changes: 39 additions & 0 deletions packages/studio-base/src/panels/createSyncRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @jest-environment jsdom */
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import { screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";

import { createSyncRoot } from "@foxglove/studio-base/panels/createSyncRoot";

describe("createSyncRoot", () => {
it("should mount the component", async () => {
const textTest = "Mount Component Test";
const TestComponent = () => <div>{textTest}</div>;

const container = document.createElement("div");
document.body.appendChild(container);

act(() => {
createSyncRoot(<TestComponent />, container);
});

expect(await screen.findByText(textTest)).toBeDefined();
});

it("should unmount the component", async () => {
const textTest = "Unmount Component Test";
const TestComponent = () => <div>{textTest}</div>;

const container = document.createElement("div");
document.body.appendChild(container);

act(() => {
const unmountCb = createSyncRoot(<TestComponent />, container);
unmountCb();
});

expect(JSON.stringify(await screen.findByText(textTest))).toBe("{}");
});
});
37 changes: 37 additions & 0 deletions packages/studio-base/src/panels/createSyncRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import { createRoot } from "react-dom/client";

/**
* Creates a synchronized root for rendering React components.
*
* This function is designed to centralize the creation of React roots
* for rendering components within a given HTML element. It addresses
* potential race conditions that may occur when mounting and unmounting
* components synchronously within the React lifecycle.
*
* By using a `setTimeout` with a minimal delay of 0 milliseconds, this
* function ensures that the rendering and unmounting operations occur
* asynchronously, allowing React to complete its current rendering cycle
* before proceeding with the next operation. This helps prevent race
* conditions and warnings related to synchronously unmounting roots.
*
* This approach was required since ReactDOM.render() was replaced by createRoot().render.
*
* @param component The JSX element to be rendered within the root.
* @param panelElement The HTML element to serve as the container for the root.
* @returns A function to unmount the root when needed.
*/
export function createSyncRoot(component: JSX.Element, panelElement: HTMLDivElement): () => void {
const root = createRoot(panelElement);
setTimeout(() => {
root.render(component);
}, 0);
return () => {
setTimeout(() => {
root.unmount();
}, 0);
};
}
Loading

0 comments on commit c929670

Please sign in to comment.