diff --git a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx index 08cbcb55ad9..4d49d44855b 100644 --- a/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx +++ b/apps/webapp/app/components/errors/ConfigureErrorAlerts.tsx @@ -103,13 +103,15 @@ export function ConfigureErrorAlerts({ } }, [fetcher.state, fetcher.data, closeHref, navigate, toast]); - const emailFieldValues = useRef( + const [emailDefaultValues] = useState(() => existingEmails.length > 0 ? [...existingEmails.map((e) => e.email), ""] : [""] ); + const emailFieldValues = useRef([...emailDefaultValues]); - const webhookFieldValues = useRef( + const [webhookDefaultValues] = useState(() => existingWebhooks.length > 0 ? [...existingWebhooks.map((w) => w.url), ""] : [""] ); + const webhookFieldValues = useRef([...webhookDefaultValues]); const [form, fields] = useForm>({ 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 }); } diff --git a/apps/webapp/app/components/navigation/SideMenu.tsx b/apps/webapp/app/components/navigation/SideMenu.tsx index d653d44dc1b..018ec84d02d 100644 --- a/apps/webapp/app/components/navigation/SideMenu.tsx +++ b/apps/webapp/app/components/navigation/SideMenu.tsx @@ -366,25 +366,32 @@ export function SideMenu({ const rafRef = useRef(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({ - 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 (
(); const [popperElement, setPopperElement] = useState(); - 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; diff --git a/apps/webapp/app/hooks/useLazyRef.ts b/apps/webapp/app/hooks/useLazyRef.ts deleted file mode 100644 index c2fc66273bb..00000000000 --- a/apps/webapp/app/hooks/useLazyRef.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MutableRefObject } from "react"; -import { useRef } from "react"; - -const useLazyRef = (initialValFunc: () => T) => { - const ref: MutableRefObject = useRef(null); - if (ref.current === null) { - ref.current = initialValFunc(); - } - return ref; -}; - -export default useLazyRef;