fix: withInternalFallback
leaking state in multi-instance scenarios (#6602)
This commit is contained in:
parent
a89952e32f
commit
a8f0a14610
100
src/components/hoc/withInternalFallback.test.tsx
Normal file
100
src/components/hoc/withInternalFallback.test.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { render, queryAllByTestId } from "../../tests/test-utils";
|
||||||
|
import { Excalidraw, MainMenu } from "../../packages/excalidraw/index";
|
||||||
|
|
||||||
|
describe("Test internal component fallback rendering", () => {
|
||||||
|
it("should render only one menu per excalidraw instance (custom menu first scenario)", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<div>
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>test</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
<Excalidraw />
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
||||||
|
|
||||||
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
|
".excalidraw-container",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render only one menu per excalidraw instance (default menu first scenario)", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<div>
|
||||||
|
<Excalidraw />
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>test</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
||||||
|
|
||||||
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
|
".excalidraw-container",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render only one menu per excalidraw instance (two custom menus scenario)", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<div>
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>test</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
<Excalidraw>
|
||||||
|
<MainMenu>test</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
||||||
|
|
||||||
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
|
".excalidraw-container",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render only one menu per excalidraw instance (two default menus scenario)", async () => {
|
||||||
|
const { container } = await render(
|
||||||
|
<div>
|
||||||
|
<Excalidraw />
|
||||||
|
<Excalidraw />
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
||||||
|
|
||||||
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
|
".excalidraw-container",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
expect(
|
||||||
|
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
||||||
|
).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import React, { useLayoutEffect } from "react";
|
import React, { useLayoutEffect, useRef } from "react";
|
||||||
import { useTunnels } from "../../context/tunnels";
|
import { useTunnels } from "../../context/tunnels";
|
||||||
|
|
||||||
export const withInternalFallback = <P,>(
|
export const withInternalFallback = <P,>(
|
||||||
@ -7,13 +7,6 @@ export const withInternalFallback = <P,>(
|
|||||||
Component: React.FC<P>,
|
Component: React.FC<P>,
|
||||||
) => {
|
) => {
|
||||||
const renderAtom = atom(0);
|
const renderAtom = atom(0);
|
||||||
// flag set on initial render to tell the fallback component to skip the
|
|
||||||
// render until mount counter are initialized. This is because the counter
|
|
||||||
// is initialized in an effect, and thus we could end rendering both
|
|
||||||
// components at the same time until counter is initialized.
|
|
||||||
let preferHost = false;
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
const WrapperComponent: React.FC<
|
const WrapperComponent: React.FC<
|
||||||
P & {
|
P & {
|
||||||
@ -21,38 +14,52 @@ export const withInternalFallback = <P,>(
|
|||||||
}
|
}
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const { jotaiScope } = useTunnels();
|
const { jotaiScope } = useTunnels();
|
||||||
const [, setRender] = useAtom(renderAtom, jotaiScope);
|
// for rerenders
|
||||||
|
const [, setCounter] = useAtom(renderAtom, jotaiScope);
|
||||||
|
// for initial & subsequent renders. Tracked as component state
|
||||||
|
// due to excalidraw multi-instance scanerios.
|
||||||
|
const metaRef = useRef({
|
||||||
|
// flag set on initial render to tell the fallback component to skip the
|
||||||
|
// render until mount counter are initialized. This is because the counter
|
||||||
|
// is initialized in an effect, and thus we could end rendering both
|
||||||
|
// components at the same time until counter is initialized.
|
||||||
|
preferHost: false,
|
||||||
|
counter: 0,
|
||||||
|
});
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
setRender((c) => {
|
const meta = metaRef.current;
|
||||||
|
setCounter((c) => {
|
||||||
const next = c + 1;
|
const next = c + 1;
|
||||||
counter = next;
|
meta.counter = next;
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
setRender((c) => {
|
setCounter((c) => {
|
||||||
const next = c - 1;
|
const next = c - 1;
|
||||||
counter = next;
|
meta.counter = next;
|
||||||
if (!next) {
|
if (!next) {
|
||||||
preferHost = false;
|
meta.preferHost = false;
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [setRender]);
|
}, [setCounter]);
|
||||||
|
|
||||||
if (!props.__fallback) {
|
if (!props.__fallback) {
|
||||||
preferHost = true;
|
metaRef.current.preferHost = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure we don't render fallback and host components at the same time
|
// ensure we don't render fallback and host components at the same time
|
||||||
if (
|
if (
|
||||||
// either before the counters are initialized
|
// either before the counters are initialized
|
||||||
(!counter && props.__fallback && preferHost) ||
|
(!metaRef.current.counter &&
|
||||||
|
props.__fallback &&
|
||||||
|
metaRef.current.preferHost) ||
|
||||||
// or after the counters are initialized, and both are rendered
|
// or after the counters are initialized, and both are rendered
|
||||||
// (this is the default when host renders as well)
|
// (this is the default when host renders as well)
|
||||||
(counter > 1 && props.__fallback)
|
(metaRef.current.counter > 1 && props.__fallback)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user