Skip to content

Commit cacde1b

Browse files
authored
fix use dimensions hook (#905)
1 parent 2f5351e commit cacde1b

File tree

11 files changed

+119
-146
lines changed

11 files changed

+119
-146
lines changed

frontend/app/block/blockframe.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
Input,
1212
} from "@/app/block/blockutil";
1313
import { Button } from "@/app/element/button";
14-
import { useWidth } from "@/app/hook/useWidth";
14+
import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions";
1515
import { TypeAheadModal } from "@/app/modals/typeaheadmodal";
1616
import { ContextMenuModel } from "@/app/store/contextmenu";
1717
import {
@@ -294,8 +294,8 @@ const ConnStatusOverlay = React.memo(
294294
const connName = blockData.meta?.connection;
295295
const connStatus = jotai.useAtomValue(getConnStatusAtom(connName));
296296
const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom);
297-
const overlayRef = React.useRef<HTMLDivElement>(null);
298-
const width = useWidth(overlayRef);
297+
const [overlayRefCallback, _, domRect] = useDimensionsWithCallbackRef(30);
298+
const width = domRect?.width;
299299
const [showError, setShowError] = React.useState(false);
300300
const blockNum = jotai.useAtomValue(nodeModel.blockNum);
301301

@@ -334,7 +334,7 @@ const ConnStatusOverlay = React.memo(
334334
}
335335

336336
return (
337-
<div className="connstatus-overlay" ref={overlayRef}>
337+
<div className="connstatus-overlay" ref={overlayRefCallback}>
338338
<div className="connstatus-content">
339339
<div className={clsx("connstatus-status-icon-wrapper", { "has-error": showError })}>
340340
{showIcon && <i className="fa-solid fa-triangle-exclamation"></i>}

frontend/app/element/menu.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { useHeight } from "@/app/hook/useHeight";
5-
import { useWidth } from "@/app/hook/useWidth";
64
import clsx from "clsx";
75
import React, { memo, useEffect, useLayoutEffect, useRef, useState } from "react";
86
import ReactDOM from "react-dom";
97

8+
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
109
import "./menu.less";
1110

1211
type MenuItem = {
@@ -143,9 +142,9 @@ const Menu = memo(
143142
const [position, setPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
144143
const menuRef = useRef<HTMLDivElement>(null);
145144
const subMenuRefs = useRef<{ [key: string]: React.RefObject<HTMLDivElement> }>({});
146-
147-
const width = useWidth(scopeRef);
148-
const height = useHeight(scopeRef);
145+
const domRect = useDimensionsWithExistingRef(scopeRef, 30);
146+
const width = domRect?.width ?? 0;
147+
const height = domRect?.height ?? 0;
149148

150149
items.forEach((_, idx) => {
151150
const key = `${idx}`;

frontend/app/hook/useDimensions.tsx

Lines changed: 93 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,99 @@
1-
import useResizeObserver from "@react-hook/resize-observer";
2-
import { useCallback, useRef, useState } from "react";
1+
import * as React from "react";
2+
import { useCallback, useState } from "react";
33
import { debounce } from "throttle-debounce";
44

5-
/**
6-
* Get the current dimensions for the specified element, and whether it is currently changing size. Update when the element resizes.
7-
* @param ref The reference to the element to observe.
8-
* @param delay The debounce delay to use for updating the dimensions.
9-
* @returns The dimensions of the element, and direction in which the dimensions are changing.
10-
*/
11-
const useDimensions = (ref: React.RefObject<HTMLElement>, delay = 0) => {
12-
const [dimensions, setDimensions] = useState<{
13-
height: number | null;
14-
width: number | null;
15-
widthDirection?: string;
16-
heightDirection?: string;
17-
}>({
18-
height: null,
19-
width: null,
20-
});
21-
22-
const previousDimensions = useRef<{ height: number | null; width: number | null }>({
23-
height: null,
24-
width: null,
25-
});
26-
27-
const updateDimensions = useCallback((entry: ResizeObserverEntry) => {
28-
const parentHeight = entry.contentRect.height;
29-
const parentWidth = entry.contentRect.width;
30-
31-
let widthDirection = "";
32-
let heightDirection = "";
33-
34-
if (previousDimensions.current.width !== null && previousDimensions.current.height !== null) {
35-
if (parentWidth > previousDimensions.current.width) {
36-
widthDirection = "expanding";
37-
} else if (parentWidth < previousDimensions.current.width) {
38-
widthDirection = "shrinking";
39-
} else {
40-
widthDirection = "unchanged";
41-
}
42-
43-
if (parentHeight > previousDimensions.current.height) {
44-
heightDirection = "expanding";
45-
} else if (parentHeight < previousDimensions.current.height) {
46-
heightDirection = "shrinking";
47-
} else {
48-
heightDirection = "unchanged";
49-
}
5+
// returns a callback ref, a ref object (that is set from the callback), and the width
6+
// pass debounceMs of null to not debounce
7+
export function useDimensionsWithCallbackRef<T extends HTMLElement>(
8+
debounceMs: number = null
9+
): [(node: T) => void, React.RefObject<T>, DOMRectReadOnly] {
10+
const [domRect, setDomRect] = useState<DOMRectReadOnly>(null);
11+
const [htmlElem, setHtmlElem] = useState<T>(null);
12+
const rszObjRef = React.useRef<ResizeObserver>(null);
13+
const oldHtmlElem = React.useRef<T>(null);
14+
const ref = React.useRef<T>(null);
15+
const refCallback = useCallback((node: T) => {
16+
setHtmlElem(node);
17+
ref.current = node;
18+
}, []);
19+
const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [
20+
debounceMs,
21+
setDomRect,
22+
]);
23+
React.useEffect(() => {
24+
if (!rszObjRef.current) {
25+
rszObjRef.current = new ResizeObserver((entries) => {
26+
for (const entry of entries) {
27+
if (domRect == null) {
28+
setDomRect(entry.contentRect);
29+
} else {
30+
setDomRectDebounced(entry.contentRect);
31+
}
32+
}
33+
});
5034
}
51-
52-
previousDimensions.current = { height: parentHeight, width: parentWidth };
53-
54-
setDimensions({ height: parentHeight, width: parentWidth, widthDirection, heightDirection });
35+
if (htmlElem) {
36+
rszObjRef.current.observe(htmlElem);
37+
oldHtmlElem.current = htmlElem;
38+
}
39+
return () => {
40+
if (oldHtmlElem.current) {
41+
rszObjRef.current?.unobserve(oldHtmlElem.current);
42+
oldHtmlElem.current = null;
43+
}
44+
};
45+
}, [htmlElem]);
46+
React.useEffect(() => {
47+
return () => {
48+
rszObjRef.current?.disconnect();
49+
};
5550
}, []);
51+
return [refCallback, ref, domRect];
52+
}
5653

57-
const fUpdateDimensions = useCallback(delay > 0 ? debounce(delay, updateDimensions) : updateDimensions, [
58-
updateDimensions,
59-
delay,
54+
// will not react to ref changes
55+
// pass debounceMs of null to not debounce
56+
export function useDimensionsWithExistingRef<T extends HTMLElement>(
57+
ref: React.RefObject<T>,
58+
debounceMs: number = null
59+
): DOMRectReadOnly {
60+
const [domRect, setDomRect] = useState<DOMRectReadOnly>(null);
61+
const rszObjRef = React.useRef<ResizeObserver>(null);
62+
const oldHtmlElem = React.useRef<T>(null);
63+
const setDomRectDebounced = React.useCallback(debounceMs == null ? setDomRect : debounce(debounceMs, setDomRect), [
64+
debounceMs,
65+
setDomRect,
6066
]);
61-
62-
useResizeObserver(ref, fUpdateDimensions);
63-
64-
return dimensions;
65-
};
66-
67-
export { useDimensions };
67+
React.useEffect(() => {
68+
if (!rszObjRef.current) {
69+
rszObjRef.current = new ResizeObserver((entries) => {
70+
for (const entry of entries) {
71+
if (domRect == null) {
72+
setDomRect(entry.contentRect);
73+
} else {
74+
setDomRectDebounced(entry.contentRect);
75+
}
76+
}
77+
});
78+
}
79+
if (ref.current) {
80+
rszObjRef.current.observe(ref.current);
81+
oldHtmlElem.current = ref.current;
82+
}
83+
return () => {
84+
if (oldHtmlElem.current) {
85+
rszObjRef.current?.unobserve(oldHtmlElem.current);
86+
oldHtmlElem.current = null;
87+
}
88+
};
89+
}, [ref.current]);
90+
React.useEffect(() => {
91+
return () => {
92+
rszObjRef.current?.disconnect();
93+
};
94+
}, []);
95+
if (ref.current != null) {
96+
return ref.current.getBoundingClientRect();
97+
}
98+
return null;
99+
}

frontend/app/hook/useHeight.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

frontend/app/hook/useWidth.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

frontend/app/modals/typeaheadmodal.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { Input } from "@/app/element/input";
55
import { InputDecoration } from "@/app/element/inputdecoration";
6-
import { useDimensions } from "@/app/hook/useDimensions";
6+
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
77
import { makeIconClass } from "@/util/util";
88
import clsx from "clsx";
99
import React, { forwardRef, useLayoutEffect, useRef } from "react";
@@ -99,7 +99,9 @@ const TypeAheadModal = ({
9999
autoFocus,
100100
selectIndex,
101101
}: TypeAheadModalProps) => {
102-
const { width, height } = useDimensions(blockRef);
102+
const domRect = useDimensionsWithExistingRef(blockRef, 30);
103+
const width = domRect?.width ?? 0;
104+
const height = domRect?.height ?? 0;
103105
const modalRef = useRef<HTMLDivElement>(null);
104106
const inputRef = useRef<HTMLDivElement>(null);
105107
const realInputRef = useRef<HTMLInputElement>(null);

frontend/app/view/cpuplot/cpuplot.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { useHeight } from "@/app/hook/useHeight";
5-
import { useWidth } from "@/app/hook/useWidth";
64
import { getConnStatusAtom, globalStore, WOS } from "@/store/global";
75
import * as util from "@/util/util";
86
import * as Plot from "@observablehq/plot";
@@ -11,6 +9,7 @@ import * as htl from "htl";
119
import * as jotai from "jotai";
1210
import * as React from "react";
1311

12+
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
1413
import { waveEventSubscribe } from "@/app/store/wps";
1514
import { RpcApi } from "@/app/store/wshclientapi";
1615
import { WindowRpcClient } from "@/app/store/wshrpcutil";
@@ -231,8 +230,9 @@ function CpuPlotView({ model, blockId }: CpuPlotViewProps) {
231230
const CpuPlotViewInner = React.memo(({ model }: CpuPlotViewProps) => {
232231
const containerRef = React.useRef<HTMLInputElement>();
233232
const plotData = jotai.useAtomValue(model.dataAtom);
234-
const parentHeight = useHeight(containerRef);
235-
const parentWidth = useWidth(containerRef);
233+
const domRect = useDimensionsWithExistingRef(containerRef, 30);
234+
const parentHeight = domRect?.height ?? 0;
235+
const parentWidth = domRect?.width ?? 0;
236236
const yvals = jotai.useAtomValue(model.metrics);
237237

238238
React.useEffect(() => {

frontend/app/view/preview/csvview.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { useHeight } from "@/app/hook/useHeight";
54
import { useTableNav } from "@table-nav/react";
65
import {
76
createColumnHelper,
@@ -14,6 +13,7 @@ import { clsx } from "clsx";
1413
import Papa from "papaparse";
1514
import { useEffect, useMemo, useRef, useState } from "react";
1615

16+
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
1717
import "./csvview.less";
1818

1919
const MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB in bytes
@@ -54,7 +54,8 @@ const CSVView = ({ parentRef, filename, content }: CSVViewProps) => {
5454
const [tableLoaded, setTableLoaded] = useState(false);
5555

5656
const { listeners } = useTableNav();
57-
const parentHeight = useHeight(parentRef);
57+
const domRect = useDimensionsWithExistingRef(parentRef, 30);
58+
const parentHeight = domRect?.height ?? 0;
5859

5960
const cacheKey = `${filename}`;
6061
csvCacheRef.current.set(cacheKey, content);

frontend/app/view/preview/directorypreview.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2024, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { useHeight } from "@/app/hook/useHeight";
54
import { ContextMenuModel } from "@/app/store/contextmenu";
65
import { atoms, createBlock, getApi } from "@/app/store/global";
76
import type { PreviewModel } from "@/app/view/preview/preview";
@@ -27,6 +26,7 @@ import { quote as shellQuote } from "shell-quote";
2726

2827
import { OverlayScrollbars } from "overlayscrollbars";
2928

29+
import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions";
3030
import "./directorypreview.less";
3131

3232
interface DirectoryTableProps {
@@ -352,8 +352,8 @@ function TableBody({
352352
const warningBoxRef = useRef<HTMLDivElement>(null);
353353
const osInstanceRef = useRef<OverlayScrollbars>(null);
354354
const rowRefs = useRef<HTMLDivElement[]>([]);
355-
356-
const parentHeight = useHeight(parentRef);
355+
const domRect = useDimensionsWithExistingRef(parentRef, 30);
356+
const parentHeight = domRect?.height ?? 0;
357357
const conn = jotai.useAtomValue(model.connection);
358358

359359
useEffect(() => {
@@ -363,7 +363,6 @@ function TableBody({
363363
const warningBoxHeight = warningBoxRef.current?.offsetHeight ?? 0;
364364
const maxHeightLessHeader = parentHeight - warningBoxHeight;
365365
const tbodyHeight = Math.min(maxHeightLessHeader, fullTBodyHeight);
366-
367366
setBodyHeight(tbodyHeight);
368367
}
369368
}, [data, parentHeight]);

0 commit comments

Comments
 (0)