Skip to content

Commit a47d519

Browse files
committed
Applied fix from @maerten #113 to avoid unmount code in useEffect() running twice in React 18 strict mode
1 parent 8b0b6ab commit a47d519

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed

src/components/Space.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CenterType, ResizeHandlePlacement, AnchorType, Type } from "../core-types";
2-
import { useSpace, ParentContext, LayerContext, DOMRectContext, IReactSpaceInnerProps } from "../core-react";
2+
import { useSpace, ParentContext, LayerContext, DOMRectContext, IReactSpaceInnerProps, useEffectOnce } from "../core-react";
33
import * as React from "react";
44
import { Centered } from "./Centered";
55
import { CenteredVertically } from "./CenteredVertically";
@@ -59,9 +59,9 @@ const SpaceInner: React.FC<IReactSpaceInnerProps & { wrapperInstance: Space }> =
5959
...{ id: props.id || props.wrapperInstance["_react_spaces_uniqueid"] },
6060
});
6161

62-
React.useEffect(() => {
62+
useEffectOnce(() => {
6363
space.element = elementRef.current!;
64-
}, []);
64+
});
6565

6666
const userClasses = className ? className.split(" ").map((c) => c.trim()) : [];
6767

src/core-react.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,56 @@
11
import * as React from "react";
22
import { createStore } from "./core";
3-
import { ISpaceProps, ISpaceStore, ISpaceDefinition, ResizeType, CenterType, ISpaceContext, ICommonProps } from "./core-types";
3+
import {
4+
ISpaceProps,
5+
ISpaceStore,
6+
ISpaceDefinition,
7+
ResizeType,
8+
CenterType,
9+
ISpaceContext,
10+
ICommonProps,
11+
ResizeMouseEvent,
12+
OnDragEnd,
13+
ResizeTouchEvent,
14+
} from "./core-types";
415
import { coalesce, shortuuid } from "./core-utils";
516
import { ResizeSensor } from "css-element-queries";
617
import * as PropTypes from "prop-types";
18+
import { useEffect, useRef, useState } from "react";
19+
20+
// WORKAROUND for React18 strict mode
21+
// https://blog.ag-grid.com/avoiding-react-18-double-mount/
22+
export const useEffectOnce = (effect: () => void | (() => void)) => {
23+
const destroyFunc = useRef<void | (() => void)>();
24+
const effectCalled = useRef(false);
25+
const renderAfterCalled = useRef(false);
26+
const [_val, setVal] = useState<number>(0);
27+
28+
if (effectCalled.current) {
29+
renderAfterCalled.current = true;
30+
}
31+
32+
useEffect(() => {
33+
// only execute the effect first time around
34+
if (!effectCalled.current) {
35+
destroyFunc.current = effect();
36+
effectCalled.current = true;
37+
}
38+
39+
// this forces one render after the effect is run
40+
setVal((val) => val + 1);
41+
42+
return () => {
43+
// if the comp didn't render since the useEffect was called,
44+
// we know it's the dummy React cycle
45+
if (!renderAfterCalled.current) {
46+
return;
47+
}
48+
if (destroyFunc.current) {
49+
destroyFunc.current();
50+
}
51+
};
52+
}, []);
53+
};
754

855
export const ParentContext = React.createContext<string | undefined>(undefined);
956
export const DOMRectContext = React.createContext<DOMRect | undefined>(undefined);
@@ -105,7 +152,7 @@ export function useSpace(props: IReactSpaceInnerProps) {
105152

106153
const resizeHandles = useSpaceResizeHandles(store, space);
107154

108-
React.useEffect(() => {
155+
useEffectOnce(() => {
109156
const rect = elementRef.current!.getBoundingClientRect() as DOMRect;
110157
space!.dimension = {
111158
...rect,
@@ -139,7 +186,7 @@ export function useSpace(props: IReactSpaceInnerProps) {
139186
resizeSensor.current && resizeSensor.current.detach();
140187
store.removeSpace(space!);
141188
};
142-
}, []);
189+
});
143190

144191
return { space: space, resizeHandles: resizeHandles, domRect: domRect, elementRef: elementRef };
145192
}
@@ -208,8 +255,14 @@ export function useCurrentSpace() {
208255

209256
const domRect = React.useContext(DOMRectContext);
210257
const layer = React.useContext(LayerContext);
211-
const onMouseDrag = React.useCallback((e, onDragEnd) => (space ? store.startMouseDrag(space, e, onDragEnd) : null), [spaceId]);
212-
const onTouchDrag = React.useCallback((e, onDragEnd) => (space ? store.startTouchDrag(space, e, onDragEnd) : null), [spaceId]);
258+
const onMouseDrag = React.useCallback(
259+
(e: ResizeMouseEvent, onDragEnd: OnDragEnd | undefined) => (space ? store.startMouseDrag(space, e, onDragEnd) : null),
260+
[spaceId],
261+
);
262+
const onTouchDrag = React.useCallback(
263+
(e: ResizeTouchEvent, onDragEnd: OnDragEnd | undefined) => (space ? store.startTouchDrag(space, e, onDragEnd) : null),
264+
[spaceId],
265+
);
213266
const onForceUpdate = React.useCallback(() => (space ? store.updateStyles(space) : null), [spaceId]);
214267

215268
const defaults = { width: 0, height: 0, x: 0, y: 0 };

0 commit comments

Comments
 (0)