Skip to content

Commit 2488e4a

Browse files
committed
refactor(webapp): derive controlled UI state during render
1 parent f8c5463 commit 2488e4a

5 files changed

Lines changed: 18 additions & 43 deletions

File tree

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
8686
ref
8787
) => {
8888
const [isChecked, setIsChecked] = useState<boolean>(defaultChecked ?? false);
89-
const [isDisabled, setIsDisabled] = useState<boolean>(disabled ?? false);
89+
const isDisabled = disabled ?? false;
9090
const onChangeRef = React.useRef(onChange);
9191
const generatedId = React.useId();
9292
const inputId = id ?? generatedId;
@@ -102,10 +102,6 @@ export const CheckboxWithLabel = React.forwardRef<HTMLInputElement, CheckboxProp
102102
const isDisabledClassName = variants[variant].isDisabled;
103103
const inputPositionClasses = variants[variant].inputPosition;
104104

105-
useEffect(() => {
106-
setIsDisabled(disabled ?? false);
107-
}, [disabled]);
108-
109105
useEffect(() => {
110106
onChangeRef.current = onChange;
111107
}, [onChange]);

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,13 @@ const ClientTabs = React.forwardRef<
2020
React.ElementRef<typeof TabsPrimitive.Root>,
2121
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
2222
>(({ onValueChange, value: valueProp, defaultValue, ...props }, ref) => {
23-
const [value, setValue] = React.useState<string | undefined>(valueProp ?? defaultValue);
24-
25-
React.useEffect(() => {
26-
if (valueProp !== undefined) {
27-
setValue(valueProp);
28-
}
29-
}, [valueProp]);
23+
const [internalValue, setInternalValue] = React.useState<string | undefined>(defaultValue);
24+
const value = valueProp ?? internalValue;
3025

3126
const handleValueChange = React.useCallback(
3227
(nextValue: string) => {
3328
if (valueProp === undefined) {
34-
setValue(nextValue);
29+
setInternalValue(nextValue);
3530
}
3631
onValueChange?.(nextValue);
3732
},

apps/webapp/app/components/runs/v3/RunTagInput.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useState, useEffect, type KeyboardEvent } from "react";
1+
import { useCallback, useState, type KeyboardEvent } from "react";
22
import { AnimatePresence, motion } from "framer-motion";
33
import { Input } from "~/components/primitives/Input";
44
import { RunTag } from "./RunTag";
@@ -26,39 +26,30 @@ export function RunTagInput({
2626
maxTagLength = 128,
2727
onTagsChange,
2828
}: TagInputProps) {
29-
// Use controlled tags if provided, otherwise use default
30-
const initialTags = controlledTags ?? defaultTags;
31-
32-
const [tags, setTags] = useState<string[]>(initialTags);
29+
const [internalTags, setInternalTags] = useState<string[]>(defaultTags);
30+
const tags = controlledTags ?? internalTags;
3331
const [inputValue, setInputValue] = useState("");
3432

35-
// Sync internal state with external tag changes
36-
useEffect(() => {
37-
if (controlledTags !== undefined) {
38-
setTags(controlledTags);
39-
}
40-
}, [controlledTags]);
41-
4233
const addTag = useCallback(
4334
(tagText: string) => {
4435
const trimmedTag = tagText.trim();
4536
if (trimmedTag && !tags.includes(trimmedTag) && tags.length < maxTags) {
4637
const newTags = [...tags, trimmedTag];
47-
setTags(newTags);
38+
if (controlledTags === undefined) setInternalTags(newTags);
4839
onTagsChange?.(newTags);
4940
}
5041
setInputValue("");
5142
},
52-
[tags, onTagsChange, maxTags]
43+
[tags, controlledTags, onTagsChange, maxTags]
5344
);
5445

5546
const removeTag = useCallback(
5647
(tagToRemove: string) => {
5748
const newTags = tags.filter((tag) => tag !== tagToRemove);
58-
setTags(newTags);
49+
if (controlledTags === undefined) setInternalTags(newTags);
5950
onTagsChange?.(newTags);
6051
},
61-
[tags, onTagsChange]
52+
[tags, controlledTags, onTagsChange]
6253
);
6354

6455
const handleKeyDown = useCallback(

apps/webapp/app/components/schedules/PurchaseSchedulesModal.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,11 @@ export function PurchaseSchedulesModal({
6767
const isLoading = fetcher.state !== "idle";
6868

6969
const [open, setOpen] = useState(false);
70-
// Reset the bundle stepper to the user's current extra-schedules count on
71-
// each open. Earlier this only re-synced when `extraSchedules`/`stepSize`
72-
// props changed, so if the user opened the modal, typed a value, cancelled,
73-
// and reopened without purchasing, the stale draft persisted.
74-
useEffect(() => {
75-
if (open) setBundles(Math.round(extraSchedules / stepSize));
76-
}, [open, extraSchedules, stepSize]);
70+
// Reset the bundle stepper to the user's current extra-schedules count on each open.
71+
const handleOpenChange = (nextOpen: boolean) => {
72+
if (nextOpen) setBundles(Math.round(extraSchedules / stepSize));
73+
setOpen(nextOpen);
74+
};
7775

7876
useEffect(() => {
7977
const data = fetcher.data;
@@ -113,7 +111,7 @@ export function PurchaseSchedulesModal({
113111
}
114112

115113
return (
116-
<Dialog open={open} onOpenChange={setOpen}>
114+
<Dialog open={open} onOpenChange={handleOpenChange}>
117115
<DialogTrigger asChild>
118116
{triggerButton ?? (
119117
<Button variant="primary/small" onClick={() => setOpen(true)}>

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
239239
};
240240

241241
export default function Page() {
242-
const [isOpen, setIsOpen] = useState(false);
243242
const { slack, option, emailAlertsEnabled } = useTypedLoaderData<typeof loader>();
244243
const lastSubmission = useActionData();
245244
const navigation = useNavigation();
@@ -274,10 +273,6 @@ export default function Page() {
274273
shouldRevalidate: "onSubmit",
275274
});
276275

277-
useEffect(() => {
278-
setIsOpen(true);
279-
}, []);
280-
281276
useEffect(() => {
282277
if (navigation.state !== "idle") return;
283278
if (lastSubmission !== undefined) return;
@@ -287,7 +282,7 @@ export default function Page() {
287282

288283
return (
289284
<Dialog
290-
open={isOpen}
285+
open
291286
onOpenChange={(o) => {
292287
if (!o) {
293288
navigate(v3ProjectAlertsPath(organization, project, environment));

0 commit comments

Comments
 (0)