refactor(webapp): derive controlled UI state during render (#4726)

## Summary

Derives controlled tab, tag, and checkbox values directly during render
instead of copying them through effects. Modal drafts now reset from
their open event, and the route-backed alert dialog renders open
immediately without a mount-time state update.
This commit is contained in:
Chris Arderne
2026-08-20 09:59:43 +01:00
committed by GitHub
parent 00149675ac
commit 101883c41c
5 changed files with 22 additions and 41 deletions
@@ -86,7 +86,7 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
ref
) => {
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
const isDisabled = disabled ?? false;
const onChangeRef = React.useRef(onChange);
const generatedId = React.useId();
const inputId = id ?? generatedId;
@@ -102,10 +102,6 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
const isDisabledClassName = variants[variant].isDisabled;
const inputPositionClasses = variants[variant].inputPosition;
useEffect(() => {
setIsDisabled(disabled ?? false);
}, [disabled]);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
@@ -20,18 +20,13 @@ const ClientTabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ onValueChange, value: valueProp, defaultValue, ...props }, ref) => {
const [value, setValue] = React.useState<string | undefined>(valueProp ?? defaultValue);
React.useEffect(() => {
if (valueProp !== undefined) {
setValue(valueProp);
}
}, [valueProp]);
const [internalValue, setInternalValue] = React.useState<string | undefined>(defaultValue);
const value = valueProp ?? internalValue;
const handleValueChange = React.useCallback(
(nextValue: string) => {
if (valueProp === undefined) {
setValue(nextValue);
setInternalValue(nextValue);
}
onValueChange?.(nextValue);
},
@@ -1,4 +1,4 @@
import { useCallback, useState, useEffect, type KeyboardEvent } from "react";
import { useCallback, useState, type KeyboardEvent } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Input } from "~/components/primitives/Input";
import { RunTag } from "./RunTag";
@@ -26,39 +26,30 @@ export function RunTagInput({
maxTagLength = 128,
onTagsChange,
}: TagInputProps) {
// Use controlled tags if provided, otherwise use default
const initialTags = controlledTags ?? defaultTags;
const [tags, setTags] = useState<string[]>(initialTags);
const [internalTags, setInternalTags] = useState<string[]>(defaultTags);
const tags = controlledTags ?? internalTags;
const [inputValue, setInputValue] = useState("");
// Sync internal state with external tag changes
useEffect(() => {
if (controlledTags !== undefined) {
setTags(controlledTags);
}
}, [controlledTags]);
const addTag = useCallback(
(tagText: string) => {
const trimmedTag = tagText.trim();
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
const newTags = [...tags, trimmedTag];
setTags(newTags);
if (controlledTags === undefined) setInternalTags(newTags);
onTagsChange?.(newTags);
}
setInputValue("");
},
[tags, onTagsChange, maxTags]
[tags, controlledTags, onTagsChange, maxTags]
);
const removeTag = useCallback(
(tagToRemove: string) => {
const newTags = tags.filter((tag) => tag !== tagToRemove);
setTags(newTags);
if (controlledTags === undefined) setInternalTags(newTags);
onTagsChange?.(newTags);
},
[tags, onTagsChange]
[tags, controlledTags, onTagsChange]
);
const handleKeyDown = useCallback(
@@ -67,12 +67,16 @@ export function PurchaseSchedulesModal({
const isLoading = fetcher.state !== "idle";
const [open, setOpen] = useState(false);
// Reset the bundle stepper to the user's current extra-schedules count on
// each open. Earlier this only re-synced when `extraSchedules`/`stepSize`
// props changed, so if the user opened the modal, typed a value, cancelled,
// and reopened without purchasing, the stale draft persisted.
// Reset the bundle stepper to the user's current extra-schedules count on each open.
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen) setBundles(Math.round(extraSchedules / stepSize));
setOpen(nextOpen);
};
useEffect(() => {
if (open) setBundles(Math.round(extraSchedules / stepSize));
if (!open) return;
// oxlint-disable-next-line react/react-compiler -- Keep the open draft aligned with authoritative billing values.
setBundles(Math.round(extraSchedules / stepSize));
}, [open, extraSchedules, stepSize]);
useEffect(() => {
@@ -113,7 +117,7 @@ export function PurchaseSchedulesModal({
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{triggerButton ?? (
<Button variant="primary/small" onClick={() => setOpen(true)}>
@@ -239,7 +239,6 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
};
export default function Page() {
const [isOpen, setIsOpen] = useState(false);
const { slack, option, emailAlertsEnabled } = useTypedLoaderData<typeof loader>();
const lastSubmission = useActionData();
const navigation = useNavigation();
@@ -274,10 +273,6 @@ export default function Page() {
shouldRevalidate: "onSubmit",
});
useEffect(() => {
setIsOpen(true);
}, []);
useEffect(() => {
if (navigation.state !== "idle") return;
if (lastSubmission !== undefined) return;
@@ -287,7 +282,7 @@ export default function Page() {
return (
<Dialog
open={isOpen}
open
onOpenChange={(o) => {
if (!o) {
navigate(v3ProjectAlertsPath(organization, project, environment));