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

[FIX] Replace React.DOM render with createRoot #174

Merged
merged 11 commits into from
Sep 11, 2024
16 changes: 8 additions & 8 deletions benchmark/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 "@lichtblick/log";
import { initI18n } from "@lichtblick/suite-base";
Expand All @@ -17,11 +17,6 @@ window.onerror = (...args) => {
console.error(...args);
};

const rootEl = document.getElementById("root");
if (!rootEl) {
throw new Error("missing #root element");
}

async function main() {
const { overwriteFetch, waitForFonts } = await import("@lichtblick/suite-base");
overwriteFetch();
Expand All @@ -32,8 +27,13 @@ async function main() {

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

// eslint-disable-next-line react/no-deprecated
ReactDOM.render(<Root />, rootEl);
const rootEl = document.getElementById("root");
if (!rootEl) {
throw new Error("missing #root element");
}

const root = createRoot(rootEl);
root.render(<Root />);
}

void main();
74 changes: 41 additions & 33 deletions packages/suite-base/src/components/Chart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,39 +304,47 @@ function Chart(props: Props): JSX.Element {
typeof canvas.transferControlToOffscreen === "function"
? canvas.transferControlToOffscreen()
: canvas;
const scales = await sendWrapperRef.current<RpcScales>(
"initialize",
{
node: offscreenCanvas,
type,
data: update.data,
typedData: update.typedData,
options: update.options,
devicePixelRatio,
width: update.width,
height: update.height,
},
[
// If this is actually a HTMLCanvasElement then it will not be transferred because we
// don't use a worker
offscreenCanvas as OffscreenCanvas,
],
);
maybeUpdateScales(scales);
onFinishRender?.();

// We cannot rely solely on the call to `initialize`, since it doesn't
// actually produce the first frame. However, if we append this update to
// the end, it will overwrite updates that have been queued _since we
// started initializing_. This is incorrect behavior and can set the
// scales incorrectly on weak devices.
//
// To prevent this from happening, we put this update at the beginning of
// the queue so that it gets coalesced properly.
queuedUpdates.current = [update, ...queuedUpdates.current];
await flushUpdates(sendWrapperRef.current);
// once we are initialized, we can allow other handlers to send to the rpc endpoint
rpcSendRef.current = sendWrapperRef.current;

// Using queueMicrotask to ensure the canvas is completely inserted into the DOM
// before sending the initialization request to the worker.
queueMicrotask(async () => {
if (!sendWrapperRef.current) {
return;
}
const scales = await sendWrapperRef.current<RpcScales>(
"initialize",
{
node: offscreenCanvas,
type,
data: update.data,
typedData: update.typedData,
options: update.options,
devicePixelRatio,
width: update.width,
height: update.height,
},
[
// If this is actually a HTMLCanvasElement then it will not be transferred because we
// don't use a worker
offscreenCanvas as OffscreenCanvas,
],
);
maybeUpdateScales(scales);
onFinishRender?.();

// We cannot rely solely on the call to `initialize`, since it doesn't
// actually produce the first frame. However, if we append this update to
// the end, it will overwrite updates that have been queued _since we
// started initializing_. This is incorrect behavior and can set the
// scales incorrectly on weak devices.
//
// To prevent this from happening, we put this update at the beginning of
// the queue so that it gets coalesced properly.
queuedUpdates.current = [update, ...queuedUpdates.current];
await flushUpdates(sendWrapperRef.current);
// once we are initialized, we can allow other handlers to send to the rpc endpoint
rpcSendRef.current = sendWrapperRef.current;
});
},
[maybeUpdateScales, onFinishRender, onStartRender, type, flushUpdates],
);
Expand Down
11 changes: 4 additions & 7 deletions packages/suite-base/src/components/DocumentDropListener.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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 "@lichtblick/suite-base/components/DocumentDropListener";
Expand All @@ -33,16 +33,15 @@ 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 @@ -65,9 +64,7 @@ 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();
});
Expand Down
10 changes: 4 additions & 6 deletions packages/suite-base/src/panels/CallService/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 { StrictMode, useMemo } from "react";
import { useMemo } from "react";

import { useCrash } from "@lichtblick/hooks";
import { PanelExtensionContext } from "@lichtblick/suite";
Expand All @@ -20,11 +20,9 @@ import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<CallService context={context} />
</CaptureErrorBoundary>
</StrictMode>,
<CaptureErrorBoundary onError={crash}>
<CallService context={context} />
</CaptureErrorBoundary>,
context.panelElement,
);
}
Expand Down
14 changes: 6 additions & 8 deletions packages/suite-base/src/panels/Gauge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 { StrictMode, useMemo } from "react";
import { useMemo } from "react";

import { useCrash } from "@lichtblick/hooks";
import { PanelExtensionContext } from "@lichtblick/suite";
Expand All @@ -21,13 +21,11 @@ import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
<Gauge context={context} />
</ThemeProvider>
</CaptureErrorBoundary>
</StrictMode>,
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
<Gauge context={context} />
</ThemeProvider>
</CaptureErrorBoundary>,
context.panelElement,
);
}
Expand Down
14 changes: 6 additions & 8 deletions packages/suite-base/src/panels/Indicator/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 { StrictMode, useMemo } from "react";
import { useMemo } from "react";

import { useCrash } from "@lichtblick/hooks";
import { PanelExtensionContext } from "@lichtblick/suite";
Expand All @@ -21,13 +21,11 @@ import { Config } from "./types";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
<Indicator context={context} />
</ThemeProvider>
</CaptureErrorBoundary>
</StrictMode>,
<CaptureErrorBoundary onError={crash}>
<ThemeProvider isDark>
<Indicator context={context} />
</ThemeProvider>
</CaptureErrorBoundary>,
context.panelElement,
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/suite-base/src/panels/StateTransitions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ function StateTransitions(props: Props) {
const xAxisHeight = 30;
return {
height: Math.max(80, onlyTopicsHeight + xAxisHeight),
heightPerTopic: onlyTopicsHeight / paths.length,
heightPerTopic: paths.length === 0 ? 0 : onlyTopicsHeight / paths.length,
};
}, [paths.length]);

Expand Down
10 changes: 4 additions & 6 deletions packages/suite-base/src/panels/Teleop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 { StrictMode, useMemo } from "react";
import { useMemo } from "react";

import { useCrash } from "@lichtblick/hooks";
import { PanelExtensionContext } from "@lichtblick/suite";
Expand All @@ -19,11 +19,9 @@ import TeleopPanel from "./TeleopPanel";

function initPanel(crash: ReturnType<typeof useCrash>, context: PanelExtensionContext) {
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<TeleopPanel context={context} />
</CaptureErrorBoundary>
</StrictMode>,
<CaptureErrorBoundary onError={crash}>
<TeleopPanel context={context} />
</CaptureErrorBoundary>,
context.panelElement,
);
}
Expand Down
24 changes: 11 additions & 13 deletions packages/suite-base/src/panels/ThreeDeeRender/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 { StrictMode, useMemo } from "react";
import { useMemo } from "react";
import { DeepPartial } from "ts-essentials";

import { useCrash } from "@lichtblick/hooks";
Expand Down Expand Up @@ -40,18 +40,16 @@ type InitPanelArgs = {
function initPanel(args: InitPanelArgs, context: BuiltinPanelExtensionContext) {
const { crash, forwardedAnalytics, interfaceMode, testOptions, customSceneExtensions } = args;
return createSyncRoot(
<StrictMode>
<CaptureErrorBoundary onError={crash}>
<ForwardAnalyticsContextProvider forwardedAnalytics={forwardedAnalytics}>
<ThreeDeeRender
context={context}
interfaceMode={interfaceMode}
testOptions={testOptions}
customSceneExtensions={customSceneExtensions}
/>
</ForwardAnalyticsContextProvider>
</CaptureErrorBoundary>
</StrictMode>,
<CaptureErrorBoundary onError={crash}>
<ForwardAnalyticsContextProvider forwardedAnalytics={forwardedAnalytics}>
<ThreeDeeRender
context={context}
interfaceMode={interfaceMode}
testOptions={testOptions}
customSceneExtensions={customSceneExtensions}
/>
</ForwardAnalyticsContextProvider>
</CaptureErrorBoundary>,
context.panelElement,
);
}
Expand Down
54 changes: 24 additions & 30 deletions packages/suite-base/src/panels/createSyncRoot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,45 @@
// 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, screen, waitFor } from "@testing-library/react";

import { createSyncRoot } from "@lichtblick/suite-base/panels/createSyncRoot";

describe("createSyncRoot", () => {
const originalError = console.error;

beforeAll(() => {
// Supress specific warning about ReactDOM.render
console.error = (...args) => {
if (args[0]?.includes("Warning: ReactDOM.render is no longer supported") === true) {
return;
}
originalError.call(console, ...args);
};
});

afterAll(() => {
// Restore original console.error after tests
console.error = originalError;
afterEach(() => {
document.body.innerHTML = "";
});

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);

createSyncRoot(<TestComponent />, container);
const text = "Mount Component Test";
const TestComponent = () => <div>{text}</div>;
act(() => {
createSyncRoot(<TestComponent />, container);
});

expect(await screen.findByText(textTest)).toBeDefined();
expect(container.innerHTML).toContain(text);
await expect(screen.findByText(text)).resolves.not.toBeUndefined();
});

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

it("should unmount the component", async () => {
const container = document.createElement("div");
document.body.appendChild(container);

const unmountCb = createSyncRoot(<TestComponent />, container);
expect(screen.queryAllByText(textTest)).toHaveLength(1);

unmountCb();
expect(screen.queryAllByText(textTest)).toHaveLength(0);
const text = "Unmount Component Test";
const TestComponent = () => <div>{text}</div>;
act(() => {
const unmount = createSyncRoot(<TestComponent />, container);
queueMicrotask(() => {
unmount();
});
});

await waitFor(() => {
expect(container.innerHTML).not.toContain(text);
expect(screen.queryByText(text)).toBeNull();
});
});
});
Loading
Loading