commit 4188248664b77aaad2d55807ee9c42d110792b84
parent f63bdce033bb271e9155d5c331b6c1b09d949175
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 02:00:21 -0400
editor: ControlledCheck/ControlledSelect put the value back after the reset only — never at mount
The first cut kept the DOM default in step in a layout effect that also wrote
the value on every commit, mount included. A choice made before hydration —
which React replays as a change event once it hydrates the element — was
overwritten by that first write, so the replayed change read the old value and
nothing changed: cadence-ui's three specs and channel-site-membership's create
case selected an option or ticked a box straight after goto and the choice was
lost.
Now a `reset` listener on the element's form puts the latest committed value
back, a microtask after the reset (React calls form.reset() inside its commit;
the layout effect only records the value). Nothing touches the DOM at mount or
on any other commit. Measured again in Chromium on react-dom 19.2.4 under
StrictMode, including a value that changes in the very commit that resets.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
1 file changed, 42 insertions(+), 23 deletions(-)
diff --git a/editor/app/components/forms/Controlled.tsx b/editor/app/components/forms/Controlled.tsx
@@ -1,9 +1,11 @@
"use client";
import {
+ useEffect,
useLayoutEffect,
useRef,
type InputHTMLAttributes,
+ type RefObject,
type SelectHTMLAttributes,
} from "react";
@@ -19,15 +21,46 @@ import {
// - a select's options' `defaultSelected` likewise (`updateOptions` is
// called with `setDefaultSelected` false on every update).
// So after ANY submit — failure or success — the reset returns the element to
-// what it mounted with while React's state still says otherwise, and nothing
-// re-renders it: the box shows the old choice, and a NAMED one posts the old
-// choice on the next submit. Measured in a browser against react-dom 19.2.4,
-// and the same code is in Next's bundled react-dom.
+// what it mounted with while React's state still says otherwise, until the
+// next re-render that reaches it re-asserts the prop (the editor's
+// auto-refresh, 5 s by default; never, with it off). A NAMED one submitted in
+// that window posts the stale choice.
//
-// These keep the DOM's default in step with the controlled value after every
-// commit, and re-assert the value itself: a layout effect runs after the
-// commit's mutations, and the reset is one of them. Same props, same markup as
+// These put the controlled value back right after the form resets: a `reset`
+// listener on the element's form, deferred a microtask so it runs after the
+// reset (React calls `form.reset()` inside its commit). They touch the DOM
+// ONLY then — never at mount, so a choice made before hydration (which React
+// replays as a change event) is not overwritten. Same props, same markup as
// the bare element.
+function useValueAfterReset<E extends HTMLInputElement | HTMLSelectElement, V>(
+ ref: RefObject<E | null>,
+ value: V,
+ put: (el: E, value: V) => void,
+) {
+ // The value of the LATEST commit, read when the microtask runs: a value that
+ // changes in the very commit that resets (a form re-reading what was stored
+ // after a save) is the one put back.
+ const latest = useRef(value);
+ useLayoutEffect(() => {
+ latest.current = value;
+ });
+ useEffect(() => {
+ const el = ref.current;
+ const form = el?.form;
+ if (!el || !form) return;
+ const onReset = () => queueMicrotask(() => put(el, latest.current));
+ form.addEventListener("reset", onReset);
+ return () => form.removeEventListener("reset", onReset);
+ }, [ref, put]);
+}
+
+const putChecked = (el: HTMLInputElement, checked: boolean) => {
+ el.checked = checked;
+};
+const putValue = (el: HTMLSelectElement, value: string) => {
+ el.value = value;
+};
+
export function ControlledCheck(
props: InputHTMLAttributes<HTMLInputElement> & {
type: "checkbox" | "radio";
@@ -35,13 +68,7 @@ export function ControlledCheck(
},
) {
const ref = useRef<HTMLInputElement>(null);
- const { checked } = props;
- useLayoutEffect(() => {
- const el = ref.current;
- if (!el) return;
- el.defaultChecked = checked;
- el.checked = checked;
- });
+ useValueAfterReset(ref, props.checked, putChecked);
return <input ref={ref} {...props} />;
}
@@ -49,14 +76,6 @@ export function ControlledSelect(
props: SelectHTMLAttributes<HTMLSelectElement> & { value: string },
) {
const ref = useRef<HTMLSelectElement>(null);
- const { value } = props;
- useLayoutEffect(() => {
- const el = ref.current;
- if (!el) return;
- for (const option of el.options) {
- option.defaultSelected = option.value === value;
- }
- el.value = value;
- });
+ useValueAfterReset(ref, props.value, putValue);
return <select ref={ref} {...props} />;
}