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.
This commit is contained in:
@@ -103,13 +103,15 @@ export function ConfigureErrorAlerts({
|
||||
}
|
||||
}, [fetcher.state, fetcher.data, closeHref, navigate, toast]);
|
||||
|
||||
const emailFieldValues = useRef<string[]>(
|
||||
const [emailDefaultValues] = useState<string[]>(() =>
|
||||
existingEmails.length > 0 ? [...existingEmails.map((e) => e.email), ""] : [""]
|
||||
);
|
||||
const emailFieldValues = useRef([...emailDefaultValues]);
|
||||
|
||||
const webhookFieldValues = useRef<string[]>(
|
||||
const [webhookDefaultValues] = useState<string[]>(() =>
|
||||
existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""]
|
||||
);
|
||||
const webhookFieldValues = useRef([...webhookDefaultValues]);
|
||||
|
||||
const [form, fields] = useForm<z.infer<typeof ErrorAlertsFormSchema>>({
|
||||
id: "configure-error-alerts",
|
||||
@@ -118,8 +120,8 @@ export function ConfigureErrorAlerts({
|
||||
},
|
||||
shouldRevalidate: "onSubmit",
|
||||
defaultValue: {
|
||||
emails: emailFieldValues.current,
|
||||
webhooks: webhookFieldValues.current,
|
||||
emails: emailDefaultValues,
|
||||
webhooks: webhookDefaultValues,
|
||||
},
|
||||
});
|
||||
const { emails, webhooks, slackChannel, slackIntegrationId } = fields;
|
||||
@@ -170,7 +172,7 @@ export function ConfigureErrorAlerts({
|
||||
emailFieldValues.current[index] = e.target.value;
|
||||
if (
|
||||
emailFields.length === emailFieldValues.current.length &&
|
||||
emailFieldValues.current.every((v) => v !== "")
|
||||
emailFieldValues.current.every((value) => value !== "")
|
||||
) {
|
||||
form.insert({ name: emails.name });
|
||||
}
|
||||
@@ -324,7 +326,7 @@ export function ConfigureErrorAlerts({
|
||||
webhookFieldValues.current[index] = e.target.value;
|
||||
if (
|
||||
webhookFields.length === webhookFieldValues.current.length &&
|
||||
webhookFieldValues.current.every((v) => v !== "")
|
||||
webhookFieldValues.current.every((value) => value !== "")
|
||||
) {
|
||||
form.insert({ name: webhooks.name });
|
||||
}
|
||||
|
||||
@@ -366,25 +366,32 @@ export function SideMenu({
|
||||
const rafRef = useRef<number | null>(null);
|
||||
// Mirror of `isCollapsed` for the drag handlers (outside React's render cycle; no stale closures).
|
||||
const isCollapsedRef = useRef(isCollapsed);
|
||||
// Freeze first-paint values so React never fights the imperative width writes after hydration.
|
||||
const [initialVisual] = useState(() => {
|
||||
const collapsed = user.dashboardPreferences.sideMenu?.isCollapsed ?? false;
|
||||
const expandedWidth = clamp(
|
||||
user.dashboardPreferences.sideMenu?.width ?? DEFAULT_WIDTH,
|
||||
DEFAULT_WIDTH,
|
||||
MAX_WIDTH
|
||||
);
|
||||
const width = collapsed ? COLLAPSED_WIDTH : expandedWidth;
|
||||
const progress = collapsed ? 1 : 0;
|
||||
|
||||
return {
|
||||
expandedWidth,
|
||||
width,
|
||||
progress,
|
||||
style: {
|
||||
width,
|
||||
"--sm-collapse": String(progress),
|
||||
"--sm-label-opacity": String(progressToLabelOpacity(progress)),
|
||||
} as CSSProperties,
|
||||
};
|
||||
});
|
||||
// The last-committed expanded width; animation targets and re-expansion read from here.
|
||||
const expandedWidthRef = useRef(
|
||||
clamp(user.dashboardPreferences.sideMenu?.width ?? DEFAULT_WIDTH, DEFAULT_WIDTH, MAX_WIDTH)
|
||||
);
|
||||
// Frozen first-paint width; never changes, so React never fights the imperative width writes.
|
||||
const initialWidthRef = useRef(
|
||||
(user.dashboardPreferences.sideMenu?.isCollapsed ?? false)
|
||||
? COLLAPSED_WIDTH
|
||||
: expandedWidthRef.current
|
||||
);
|
||||
const widthRef = useRef(initialWidthRef.current);
|
||||
const progressRef = useRef((user.dashboardPreferences.sideMenu?.isCollapsed ?? false) ? 1 : 0);
|
||||
// Frozen initial style (incl. CSS vars) so the SSR HTML has the right collapsed/expanded visuals
|
||||
// (no pre-hydration flash). Stable identity, so React never rewrites it after writeVisual.
|
||||
const initialStyleRef = useRef<CSSProperties>({
|
||||
width: initialWidthRef.current,
|
||||
"--sm-collapse": String(progressRef.current),
|
||||
"--sm-label-opacity": String(progressToLabelOpacity(progressRef.current)),
|
||||
} as CSSProperties);
|
||||
const expandedWidthRef = useRef(initialVisual.expandedWidth);
|
||||
const widthRef = useRef(initialVisual.width);
|
||||
const progressRef = useRef(initialVisual.progress);
|
||||
// Removes an in-flight drag's window listeners (set on pointerdown; cleared on finish/unmount).
|
||||
const dragCleanupRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -1066,7 +1073,7 @@ export function SideMenu({
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
style={initialStyleRef.current}
|
||||
style={initialVisual.style}
|
||||
className={cn(
|
||||
"relative h-full border-r bg-background-bright",
|
||||
// The accent is the loudest "you are not this user" tell, so "view as user" drops it too —
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { usePopper } from "react-popper";
|
||||
import { useEvent } from "react-use";
|
||||
import useLazyRef from "~/hooks/useLazyRef";
|
||||
|
||||
// Recharts 3.x will have portal support, but until then we're using this:
|
||||
//https://github.com/recharts/recharts/issues/2458#issuecomment-1063463873
|
||||
@@ -33,13 +32,9 @@ export interface PopperPortalProps {
|
||||
export default function TooltipPortal({ active = true, children }: PopperPortalProps) {
|
||||
const [portalElement, setPortalElement] = useState<HTMLDivElement>();
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>();
|
||||
const virtualElementRef = useLazyRef(() => new VirtualElement());
|
||||
const [virtualElement] = useState(() => new VirtualElement());
|
||||
|
||||
const { styles, attributes, update } = usePopper(
|
||||
virtualElementRef.current,
|
||||
popperElement,
|
||||
POPPER_OPTIONS
|
||||
);
|
||||
const { styles, attributes, update } = usePopper(virtualElement, popperElement, POPPER_OPTIONS);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.createElement("div");
|
||||
@@ -50,7 +45,7 @@ export default function TooltipPortal({ active = true, children }: PopperPortalP
|
||||
}, []);
|
||||
|
||||
useEvent("mousemove", ({ clientX: x, clientY: y }) => {
|
||||
virtualElementRef.current?.update(x, y);
|
||||
virtualElement.update(x, y);
|
||||
if (!active) return;
|
||||
update?.();
|
||||
});
|
||||
@@ -59,9 +54,9 @@ export default function TooltipPortal({ active = true, children }: PopperPortalP
|
||||
if (!active) return;
|
||||
// Seed from the last known pointer so the tooltip appears at the cursor immediately, even if the
|
||||
// mouse is held still after hovering onto a point (otherwise it flashes in the top-left corner).
|
||||
virtualElementRef.current?.update(lastPointer.x, lastPointer.y);
|
||||
virtualElement.update(lastPointer.x, lastPointer.y);
|
||||
update?.();
|
||||
}, [active, update, virtualElementRef]);
|
||||
}, [active, update, virtualElement]);
|
||||
|
||||
if (!portalElement) return null;
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useRef } from "react";
|
||||
|
||||
const useLazyRef = <T>(initialValFunc: () => T) => {
|
||||
const ref: MutableRefObject<T | null> = useRef(null);
|
||||
if (ref.current === null) {
|
||||
ref.current = initialValFunc();
|
||||
}
|
||||
return ref;
|
||||
};
|
||||
|
||||
export default useLazyRef;
|
||||
Reference in New Issue
Block a user