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);