Skip to content

Commit a89ce5a

Browse files
authored
refactor(webapp): replace render-time ref initialization (#4729)
## Summary Replaces render-time ref initialization with lazy state for frozen form defaults, the tooltip's virtual positioning element, and the side menu's first-paint visuals. Editable alert fields now update immutable state snapshots.
1 parent 7ab437c commit a89ce5a

4 files changed

Lines changed: 39 additions & 47 deletions

File tree

apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,15 @@ export function ConfigureErrorAlerts({
103103
}
104104
}, [fetcher.state, fetcher.data, closeHref, navigate, toast]);
105105

106-
const emailFieldValues = useRef<string[]>(
106+
const [emailDefaultValues] = useState<string[]>(() =>
107107
existingEmails.length > 0 ? [...existingEmails.map((e) => e.email), ""] : [""]
108108
);
109+
const emailFieldValues = useRef([...emailDefaultValues]);
109110

110-
const webhookFieldValues = useRef<string[]>(
111+
const [webhookDefaultValues] = useState<string[]>(() =>
111112
existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""]
112113
);
114+
const webhookFieldValues = useRef([...webhookDefaultValues]);
113115

114116
const [form, fields] = useForm<z.infer<typeof ErrorAlertsFormSchema>>({
115117
id: "configure-error-alerts",
@@ -118,8 +120,8 @@ export function ConfigureErrorAlerts({
118120
},
119121
shouldRevalidate: "onSubmit",
120122
defaultValue: {
121-
emails: emailFieldValues.current,
122-
webhooks: webhookFieldValues.current,
123+
emails: emailDefaultValues,
124+
webhooks: webhookDefaultValues,
123125
},
124126
});
125127
const { emails, webhooks, slackChannel, slackIntegrationId } = fields;
@@ -170,7 +172,7 @@ export function ConfigureErrorAlerts({
170172
emailFieldValues.current[index] = e.target.value;
171173
if (
172174
emailFields.length === emailFieldValues.current.length &&
173-
emailFieldValues.current.every((v) => v !== "")
175+
emailFieldValues.current.every((value) => value !== "")
174176
) {
175177
form.insert({ name: emails.name });
176178
}
@@ -324,7 +326,7 @@ export function ConfigureErrorAlerts({
324326
webhookFieldValues.current[index] = e.target.value;
325327
if (
326328
webhookFields.length === webhookFieldValues.current.length &&
327-
webhookFieldValues.current.every((v) => v !== "")
329+
webhookFieldValues.current.every((value) => value !== "")
328330
) {
329331
form.insert({ name: webhooks.name });
330332
}

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -366,25 +366,32 @@ export function SideMenu({
366366
const rafRef = useRef<number | null>(null);
367367
// Mirror of `isCollapsed` for the drag handlers (outside React's render cycle; no stale closures).
368368
const isCollapsedRef = useRef(isCollapsed);
369+
// Freeze first-paint values so React never fights the imperative width writes after hydration.
370+
const [initialVisual] = useState(() => {
371+
const collapsed = user.dashboardPreferences.sideMenu?.isCollapsed ?? false;
372+
const expandedWidth = clamp(
373+
user.dashboardPreferences.sideMenu?.width ?? DEFAULT_WIDTH,
374+
DEFAULT_WIDTH,
375+
MAX_WIDTH
376+
);
377+
const width = collapsed ? COLLAPSED_WIDTH : expandedWidth;
378+
const progress = collapsed ? 1 : 0;
379+
380+
return {
381+
expandedWidth,
382+
width,
383+
progress,
384+
style: {
385+
width,
386+
"--sm-collapse": String(progress),
387+
"--sm-label-opacity": String(progressToLabelOpacity(progress)),
388+
} as CSSProperties,
389+
};
390+
});
369391
// The last-committed expanded width; animation targets and re-expansion read from here.
370-
const expandedWidthRef = useRef(
371-
clamp(user.dashboardPreferences.sideMenu?.width ?? DEFAULT_WIDTH, DEFAULT_WIDTH, MAX_WIDTH)
372-
);
373-
// Frozen first-paint width; never changes, so React never fights the imperative width writes.
374-
const initialWidthRef = useRef(
375-
(user.dashboardPreferences.sideMenu?.isCollapsed ?? false)
376-
? COLLAPSED_WIDTH
377-
: expandedWidthRef.current
378-
);
379-
const widthRef = useRef(initialWidthRef.current);
380-
const progressRef = useRef((user.dashboardPreferences.sideMenu?.isCollapsed ?? false) ? 1 : 0);
381-
// Frozen initial style (incl. CSS vars) so the SSR HTML has the right collapsed/expanded visuals
382-
// (no pre-hydration flash). Stable identity, so React never rewrites it after writeVisual.
383-
const initialStyleRef = useRef<CSSProperties>({
384-
width: initialWidthRef.current,
385-
"--sm-collapse": String(progressRef.current),
386-
"--sm-label-opacity": String(progressToLabelOpacity(progressRef.current)),
387-
} as CSSProperties);
392+
const expandedWidthRef = useRef(initialVisual.expandedWidth);
393+
const widthRef = useRef(initialVisual.width);
394+
const progressRef = useRef(initialVisual.progress);
388395
// Removes an in-flight drag's window listeners (set on pointerdown; cleared on finish/unmount).
389396
const dragCleanupRef = useRef<(() => void) | null>(null);
390397

@@ -1066,7 +1073,7 @@ export function SideMenu({
10661073
return (
10671074
<div
10681075
ref={rootRef}
1069-
style={initialStyleRef.current}
1076+
style={initialVisual.style}
10701077
className={cn(
10711078
"relative h-full border-r bg-background-bright",
10721079
// The accent is the loudest "you are not this user" tell, so "view as user" drops it too —

apps/webapp/app/components/primitives/TooltipPortal.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
44
import { createPortal } from "react-dom";
55
import { usePopper } from "react-popper";
66
import { useEvent } from "react-use";
7-
import useLazyRef from "~/hooks/useLazyRef";
87

98
// Recharts 3.x will have portal support, but until then we're using this:
109
//https://github.com/recharts/recharts/issues/2458#issuecomment-1063463873
@@ -33,13 +32,9 @@ export interface PopperPortalProps {
3332
export default function TooltipPortal({ active = true, children }: PopperPortalProps) {
3433
const [portalElement, setPortalElement] = useState<HTMLDivElement>();
3534
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
36-
const virtualElementRef = useLazyRef(() => new VirtualElement());
35+
const [virtualElement] = useState(() => new VirtualElement());
3736

38-
const { styles, attributes, update } = usePopper(
39-
virtualElementRef.current,
40-
popperElement,
41-
POPPER_OPTIONS
42-
);
37+
const { styles, attributes, update } = usePopper(virtualElement, popperElement, POPPER_OPTIONS);
4338

4439
useEffect(() => {
4540
const el = document.createElement("div");
@@ -50,7 +45,7 @@ export default function TooltipPortal({ active = true, children }: PopperPortalP
5045
}, []);
5146

5247
useEvent("mousemove", ({ clientX: x, clientY: y }) => {
53-
virtualElementRef.current?.update(x, y);
48+
virtualElement.update(x, y);
5449
if (!active) return;
5550
update?.();
5651
});
@@ -59,9 +54,9 @@ export default function TooltipPortal({ active = true, children }: PopperPortalP
5954
if (!active) return;
6055
// Seed from the last known pointer so the tooltip appears at the cursor immediately, even if the
6156
// mouse is held still after hovering onto a point (otherwise it flashes in the top-left corner).
62-
virtualElementRef.current?.update(lastPointer.x, lastPointer.y);
57+
virtualElement.update(lastPointer.x, lastPointer.y);
6358
update?.();
64-
}, [active, update, virtualElementRef]);
59+
}, [active, update, virtualElement]);
6560

6661
if (!portalElement) return null;
6762

apps/webapp/app/hooks/useLazyRef.ts

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

0 commit comments

Comments
 (0)