diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 03b9076b338..96021e305ae 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -2281,4 +2281,64 @@ describe('ReactDOMForm', () => { await submit(formRef.current); assertLog(['stringified action']); }); + + it('form actions should retain status when nested state changes', async () => { + const formRef = React.createRef(); + + let rerenderUnrelatedStatus; + function UnrelatedStatus() { + const {pending} = useFormStatus(); + const [counter, setCounter] = useState(0); + rerenderUnrelatedStatus = () => setCounter(n => n + 1); + Scheduler.log(`[unrelated form] pending: ${pending}, state: ${counter}`); + } + + let rerenderTargetStatus; + function TargetStatus() { + const {pending} = useFormStatus(); + const [counter, setCounter] = useState(0); + Scheduler.log(`[target form] pending: ${pending}, state: ${counter}`); + rerenderTargetStatus = () => setCounter(n => n + 1); + } + + function App() { + async function action() { + return new Promise(resolve => { + // never resolves + }); + } + + return ( + <> +
+ + + +
+ + + + ); + } + + const root = ReactDOMClient.createRoot(container); + await act(() => root.render()); + + assertLog([ + '[target form] pending: false, state: 0', + '[unrelated form] pending: false, state: 0', + ]); + + await submit(formRef.current); + + assertLog(['[target form] pending: true, state: 0']); + + await act(() => rerenderTargetStatus()); + + assertLog(['[target form] pending: true, state: 1']); + + await act(() => rerenderUnrelatedStatus()); + + assertLog(['[unrelated form] pending: false, state: 1']); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 7020af6e043..ba401a55d4b 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1974,7 +1974,7 @@ function updateHostComponent( // If the transition state changed, propagate the change to all the // descendents. We use Context as an implementation detail for this. // - // This is intentionally set here instead of pushHostContext because + // We need to update it here because // pushHostContext gets called before we process the state hook, to avoid // a state mismatch in the event that something suspends. // diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 10ea377fc2b..2d2ec4c88ac 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -9,7 +9,11 @@ import type {Fiber} from './ReactInternalTypes'; import type {StackCursor} from './ReactFiberStack'; -import type {Container, HostContext} from './ReactFiberConfig'; +import type { + Container, + HostContext, + TransitionStatus, +} from './ReactFiberConfig'; import type {Hook} from './ReactFiberHooks'; import { @@ -92,6 +96,21 @@ function getHostContext(): HostContext { function pushHostContext(fiber: Fiber): void { const stateHook: Hook | null = fiber.memoizedState; if (stateHook !== null) { + // Propagate the current state to all the descendents. + // We use Context as an implementation detail for this. + // + // NOTE: This assumes that there cannot be nested transition providers, + // because the only renderer that implements this feature is React DOM, + // and forms cannot be nested. If we did support nested providers, then + // we would need to push a context value even for host fibers that + // haven't been upgraded yet. + const transitionStatus: TransitionStatus = stateHook.memoizedState; + if (isPrimaryRenderer) { + HostTransitionContext._currentValue = transitionStatus; + } else { + HostTransitionContext._currentValue2 = transitionStatus; + } + // Only provide context if this fiber has been upgraded by a host // transition. We use the same optimization for regular host context below. push(hostTransitionProviderCursor, fiber, fiber);