Skip to content

Commit

Permalink
refactor useMeasure
Browse files Browse the repository at this point in the history
  • Loading branch information
rpominov committed Nov 10, 2022
1 parent fed1f81 commit e625295
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 76 deletions.
11 changes: 3 additions & 8 deletions apps/designer/app/canvas/shared/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
useRootInstance,
useTextEditingInstanceId,
} from "~/shared/nano-states";
import { useMeasure } from "~/shared/dom-hooks";
import { useMeasure } from "~/canvas/shared/use-measure";
import {
findInstanceByElement,
getInstanceElementById,
Expand Down Expand Up @@ -254,15 +254,10 @@ const publishRect = (rect: DOMRect) => {

export const usePublishSelectedInstanceDataRect = () => {
const [element] = useSelectedElement();
const [refCallback, rect] = useMeasure();
const rect = useMeasure(element);

useEffect(() => {
// Disconnect observer when there is no element.
refCallback(element ?? null);
}, [element, refCallback]);

useEffect(() => {
if (rect !== undefined) publishRect(rect);
if (rect) publishRect(rect);
}, [rect]);
};

Expand Down
47 changes: 47 additions & 0 deletions apps/designer/app/canvas/shared/use-measure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useCallback, useEffect, useMemo } from "react";
import { useMeasure as useMeasureBase, useRectState } from "~/shared/dom-hooks";
import { type PubsubMap, useSubscribeAll } from "~/shared/pubsub";

const useTreeChange = (onChange: () => void, enabled: boolean) => {
const callback = useMemo(() => {
if (!enabled) {
return () => null;
}
return (type: keyof PubsubMap) => {
if (
type === "updateProps" ||
type === "deleteProp" ||
type === "insertInstance" ||
type === "deleteInstance" ||
type === "reparentInstance" ||
type === "updateStyle" ||
type.startsWith("previewStyle:")
) {
onChange();
}
};
}, [onChange, enabled]);

useSubscribeAll(callback);
};

// A version of useMeasure capable of measuring inlined elements, but works only within canvas.
export const useMeasure = (element: HTMLElement | undefined) => {
const { canObserve, rect: baseRect } = useMeasureBase(element);

const [rect, setRect] = useRectState();

const handleChange = useCallback(() => {
setRect(element && element.getBoundingClientRect());
}, [element, setRect]);

useTreeChange(handleChange, canObserve === false);

useEffect(() => {
if (canObserve === false) {
handleChange();
}
}, [handleChange, canObserve]);

return canObserve ? baseRect : rect;
};
111 changes: 43 additions & 68 deletions apps/designer/app/shared/dom-hooks/use-measure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,46 @@
// We have to use getBoundingClientRect instead.
// @todo optimize for the case when many consumers need to measure the same element

import { useCallback, useEffect, useMemo, useState } from "react";
import { PubsubMap, useSubscribeAll } from "~/shared/pubsub";
import { useCallback, useEffect, useState } from "react";
import { useScrollState } from "./use-scroll-state";

export type UseMeasureRef<MeasuredElement extends HTMLElement = HTMLElement> = (
element: MeasuredElement | null
) => void;
export type UseMeasureResult<
MeasuredElement extends HTMLElement = HTMLElement
> = [UseMeasureRef<MeasuredElement>, DOMRect | undefined];

export const useMeasure = <
MeasuredElement extends HTMLElement = HTMLElement
>(): UseMeasureResult<MeasuredElement> => {
const [element, setElement] = useState<MeasuredElement | null>(null);
const [rect, setRect] = useState<DOMRect>();
export const useRectState = () => {
const [rect, setRectBase] = useState<DOMRect>();
const setRect = useCallback(
(nextRect: DOMRect | undefined) =>
setRectBase((currentRect) => {
if (
currentRect === undefined ||
nextRect === undefined ||
currentRect.x !== nextRect.x ||
currentRect.y !== nextRect.y ||
currentRect.width !== nextRect.width ||
currentRect.height !== nextRect.height
) {
return nextRect;
}
return currentRect;
}),
[]
);
return [rect, setRect] as const;
};

export const useMeasure = (
element: HTMLElement | undefined
): { canObserve: boolean; rect: DOMRect | undefined } => {
const [rect, setRect] = useRectState();
const [isInline, setIsInline] = useState(false);

const handleChange = useCallback(() => {
if (element === null || typeof window === "undefined") return;
setIsInline(window.getComputedStyle(element).display === "inline");
const nextRect = element.getBoundingClientRect();
setRect((currentRect) => {
if (
currentRect === undefined ||
currentRect.x !== nextRect.x ||
currentRect.y !== nextRect.y ||
currentRect.width !== nextRect.width ||
currentRect.height !== nextRect.height
) {
return nextRect;
}
return currentRect;
});
}, [element]);

// ResizeObserver does not work for inline elements,
// so if the element is inline, we measure it after any change in the instance tree.
//
// NOTE: We assume useMeasure is used only to measure elements on canvas.
// If this ever changes, we need to refactor this.
useTreeChange(handleChange, isInline);
const triggerMeasure = useCallback(() => {
setRect(element && element.getBoundingClientRect());
setIsInline(
element ? window.getComputedStyle(element).display === "inline" : false
);
}, [element, setRect]);

useScrollState({
onScrollEnd: handleChange,
onScrollEnd: triggerMeasure,
});

// Detect movement of the element without remounting.
Expand All @@ -57,44 +51,25 @@ export const useMeasure = <
// React cannot do that. It can only move within the same parent.
const parent = element?.parentElement;
if (parent) {
const observer = new window.MutationObserver(handleChange);
const observer = new window.MutationObserver(triggerMeasure);
observer.observe(parent, { childList: true });
return () => observer.disconnect();
}
}, [element, handleChange]);
}, [element, triggerMeasure]);

useEffect(() => {
if (element) {
const observer = new window.ResizeObserver(handleChange);
const observer = new window.ResizeObserver(triggerMeasure);
observer.observe(element);
return () => observer.disconnect();
}
}, [element, handleChange]);

useEffect(handleChange, [handleChange]);
}, [element, triggerMeasure]);

return [setElement, rect];
};

const useTreeChange = (onChange: () => void, enabled: boolean) => {
const callback = useMemo(() => {
if (!enabled) {
return () => null;
}
return (type: keyof PubsubMap) => {
if (
type === "updateProps" ||
type === "deleteProp" ||
type === "insertInstance" ||
type === "deleteInstance" ||
type === "reparentInstance" ||
type === "updateStyle" ||
type.startsWith("previewStyle:")
) {
onChange();
}
};
}, [onChange, enabled]);
useEffect(triggerMeasure, [triggerMeasure]);

useSubscribeAll(callback);
return {
// ResizeObserver does not work for inline elements
canObserve: isInline === false,
rect,
};
};

0 comments on commit e625295

Please sign in to comment.