@samitouri / QOS-React-1 / commits / 8ac5f4eb36

Fix form status reset when component state is updated (#34075)

Co-authored-by: Vordgi <sasha2822222@gmail.com>

Sebastian "Sebbie" Silbermann committed Nov 19, 2025 at 18:22 UTC 8ac5f4eb3601f7381462f8b74ecf24d47259cc20
3 files changed +81 -2
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+60
@@ -2281,4 +2281,64 @@ describe('ReactDOMForm', () => {
2281 await submit(formRef.current);
2282 assertLog(['stringified action']);
2283 });
2284 +
2285 + it('form actions should retain status when nested state changes', async () => {
2286 + const formRef = React.createRef();
2287 +
2288 + let rerenderUnrelatedStatus;
2289 + function UnrelatedStatus() {
2290 + const {pending} = useFormStatus();
2291 + const [counter, setCounter] = useState(0);
2292 + rerenderUnrelatedStatus = () => setCounter(n => n + 1);
2293 + Scheduler.log(`[unrelated form] pending: ${pending}, state: ${counter}`);
2294 + }
2295 +
2296 + let rerenderTargetStatus;
2297 + function TargetStatus() {
2298 + const {pending} = useFormStatus();
2299 + const [counter, setCounter] = useState(0);
2300 + Scheduler.log(`[target form] pending: ${pending}, state: ${counter}`);
2301 + rerenderTargetStatus = () => setCounter(n => n + 1);
2302 + }
2303 +
2304 + function App() {
2305 + async function action() {
2306 + return new Promise(resolve => {
2307 + // never resolves
2308 + });
2309 + }
2310 +
2311 + return (
2312 + <>
2313 + <form action={action} ref={formRef}>
2314 + <input type="submit" />
2315 + <TargetStatus />
2316 + </form>
2317 + <form>
2318 + <UnrelatedStatus />
2319 + </form>
2320 + </>
2321 + );
2322 + }
2323 +
2324 + const root = ReactDOMClient.createRoot(container);
2325 + await act(() => root.render(<App />));
2326 +
2327 + assertLog([
2328 + '[target form] pending: false, state: 0',
2329 + '[unrelated form] pending: false, state: 0',
2330 + ]);
2331 +
2332 + await submit(formRef.current);
2333 +
2334 + assertLog(['[target form] pending: true, state: 0']);
2335 +
2336 + await act(() => rerenderTargetStatus());
2337 +
2338 + assertLog(['[target form] pending: true, state: 1']);
2339 +
2340 + await act(() => rerenderUnrelatedStatus());
2341 +
2342 + assertLog(['[unrelated form] pending: false, state: 1']);
2343 + });
2344 });
packages/react-reconciler/src/ReactFiberBeginWork.js
+1 -1
@@ -1974,7 +1974,7 @@ function updateHostComponent(
1974 // If the transition state changed, propagate the change to all the
1975 // descendents. We use Context as an implementation detail for this.
1976 //
1977 - // This is intentionally set here instead of pushHostContext because
1977 + // We need to update it here because
1978 // pushHostContext gets called before we process the state hook, to avoid
1979 // a state mismatch in the event that something suspends.
1980 //
packages/react-reconciler/src/ReactFiberHostContext.js
+20 -1
@@ -9,7 +9,11 @@
9
10 import type {Fiber} from './ReactInternalTypes';
11 import type {StackCursor} from './ReactFiberStack';
12 -import type {Container, HostContext} from './ReactFiberConfig';
12 +import type {
13 + Container,
14 + HostContext,
15 + TransitionStatus,
16 +} from './ReactFiberConfig';
17 import type {Hook} from './ReactFiberHooks';
18
19 import {
@@ -92,6 +96,21 @@ function getHostContext(): HostContext {
96 function pushHostContext(fiber: Fiber): void {
97 const stateHook: Hook | null = fiber.memoizedState;
98 if (stateHook !== null) {
99 + // Propagate the current state to all the descendents.
100 + // We use Context as an implementation detail for this.
101 + //
102 + // NOTE: This assumes that there cannot be nested transition providers,
103 + // because the only renderer that implements this feature is React DOM,
104 + // and forms cannot be nested. If we did support nested providers, then
105 + // we would need to push a context value even for host fibers that
106 + // haven't been upgraded yet.
107 + const transitionStatus: TransitionStatus = stateHook.memoizedState;
108 + if (isPrimaryRenderer) {
109 + HostTransitionContext._currentValue = transitionStatus;
110 + } else {
111 + HostTransitionContext._currentValue2 = transitionStatus;
112 + }
113 +
114 // Only provide context if this fiber has been upgraded by a host
115 // transition. We use the same optimization for regular host context below.
116 push(hostTransitionProviderCursor, fiber, fiber);