From 88778ab640b50de15c992a757347aae57a75288a Mon Sep 17 00:00:00 2001 From: acdlite Date: Thu, 7 Sep 2023 20:10:56 +0000 Subject: [PATCH] useFormState: Emit comment to mark whether state matches (#27307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A planned feature of useFormState is that if the page load is the result of an MPA-style form submission — i.e. a form was submitted before it was hydrated, using Server Actions — the state of the hook should transfer to the next page. I haven't implemented that part yet, but as a prerequisite, we need some way for Fizz to indicate whether a useFormState hook was rendered using the "postback" state. That way we can do all state matching logic on the server without having to replicate it on the client, too. The approach here is to emit a comment node for each useFormState hook. We use one of two comment types: `` for a normal useFormState hook, and `` for a hook that was rendered using the postback state. React will read these markers during hydration. This is similar to how we encode Suspense boundaries. Again, the actual matching algorithm is not yet implemented — for now, the "not matching" marker is always emitted. We can optimize this further by not emitting any markers for a render that is not the result of a form postback, which I'll do in subsequent PRs. DiffTrain build for [8b26f07a883bb341c20283c0099bf5ee6f87bd1f](https://github.com/facebook/react/commit/8b26f07a883bb341c20283c0099bf5ee6f87bd1f) --- compiled/facebook-www/REVISION | 2 +- compiled/facebook-www/ReactDOM-dev.classic.js | 6 +- compiled/facebook-www/ReactDOM-dev.modern.js | 6 +- .../ReactDOMServer-dev.classic.js | 155 +++++++++++++----- .../facebook-www/ReactDOMServer-dev.modern.js | 155 +++++++++++++----- .../ReactDOMServer-prod.classic.js | 72 ++++++-- .../ReactDOMServer-prod.modern.js | 72 ++++++-- .../ReactDOMServerStreaming-dev.modern.js | 153 +++++++++++++---- .../ReactDOMServerStreaming-prod.modern.js | 70 ++++++-- .../ReactDOMTesting-dev.classic.js | 6 +- .../ReactDOMTesting-dev.modern.js | 6 +- .../ReactTestRenderer-dev.classic.js | 2 +- .../ReactTestRenderer-dev.modern.js | 2 +- 13 files changed, 545 insertions(+), 162 deletions(-) diff --git a/compiled/facebook-www/REVISION b/compiled/facebook-www/REVISION index d4364b2c63e69..5e939f9018c75 100644 --- a/compiled/facebook-www/REVISION +++ b/compiled/facebook-www/REVISION @@ -1 +1 @@ -3566de59e2046e7e8478462375aaa71716f1095b +8b26f07a883bb341c20283c0099bf5ee6f87bd1f diff --git a/compiled/facebook-www/ReactDOM-dev.classic.js b/compiled/facebook-www/ReactDOM-dev.classic.js index 1559bfbd38a9f..cd142883df5a6 100644 --- a/compiled/facebook-www/ReactDOM-dev.classic.js +++ b/compiled/facebook-www/ReactDOM-dev.classic.js @@ -159,6 +159,7 @@ var enableHostSingletons = true; var enableClientRenderFallbackOnTextMismatch = false; var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation. +var enableFormActions = false; var enableSuspenseCallback = true; var FunctionComponent = 0; @@ -34008,7 +34009,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-classic-4045cb9c"; +var ReactVersion = "18.3.0-www-classic-5b9b66e9"; function createPortal$1( children, @@ -42938,7 +42939,8 @@ function getNextHydratable(node) { if ( nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || - nodeData === SUSPENSE_PENDING_START_DATA + nodeData === SUSPENSE_PENDING_START_DATA || + enableFormActions ) { break; } diff --git a/compiled/facebook-www/ReactDOM-dev.modern.js b/compiled/facebook-www/ReactDOM-dev.modern.js index b9b4d7df79859..31fac7c316a60 100644 --- a/compiled/facebook-www/ReactDOM-dev.modern.js +++ b/compiled/facebook-www/ReactDOM-dev.modern.js @@ -145,6 +145,7 @@ var enableHostSingletons = true; var enableClientRenderFallbackOnTextMismatch = false; var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation. +var enableFormActions = false; var enableSuspenseCallback = true; var ReactSharedInternals = @@ -33853,7 +33854,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-modern-73585f43"; +var ReactVersion = "18.3.0-www-modern-dd5c91c5"; function createPortal$1( children, @@ -43448,7 +43449,8 @@ function getNextHydratable(node) { if ( nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || - nodeData === SUSPENSE_PENDING_START_DATA + nodeData === SUSPENSE_PENDING_START_DATA || + enableFormActions ) { break; } diff --git a/compiled/facebook-www/ReactDOMServer-dev.classic.js b/compiled/facebook-www/ReactDOMServer-dev.classic.js index f65bcded1be1a..76646288a21c8 100644 --- a/compiled/facebook-www/ReactDOMServer-dev.classic.js +++ b/compiled/facebook-www/ReactDOMServer-dev.classic.js @@ -19,7 +19,7 @@ if (__DEV__) { var React = require("react"); var ReactDOM = require("react-dom"); -var ReactVersion = "18.3.0-www-classic-605a9ce6"; +var ReactVersion = "18.3.0-www-classic-de28fc21"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -3153,6 +3153,15 @@ function pushStartOption(target, props, formatContext) { return children; } +var formStateMarkerIsMatching = stringToPrecomputedChunk(""); +var formStateMarkerIsNotMatching = stringToPrecomputedChunk(""); +function pushFormStateMarkerIsMatching(target) { + target.push(formStateMarkerIsMatching); +} +function pushFormStateMarkerIsNotMatching(target) { + target.push(formStateMarkerIsNotMatching); +} + function pushStartForm(target, props, resumableState, renderState) { target.push(startChunkForTag("form")); var children = null; @@ -9152,7 +9161,14 @@ var isReRender = false; // Whether an update was scheduled during the currently var didScheduleRenderPhaseUpdate = false; // Counts the number of useId hooks in this component -var localIdCounter = 0; // Counts the number of use(thenable) calls in this component +var localIdCounter = 0; // Chunks that should be pushed to the stream once the component +// finishes rendering. +// Counts the number of useFormState calls in this component + +var formStateCounter = 0; // The index of the useFormState hook that matches the one passed in at the +// root during an MPA navigation, if any. + +var formStateMatchingIndex = -1; // Counts the number of use(thenable) calls in this component var thenableIndexCounter = 0; var thenableState = null; // Lazily created map of render-phase updates @@ -9285,6 +9301,8 @@ function prepareToUseHooks(task, componentIdentity, prevThenableState) { // workInProgressHook = null; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; thenableState = prevThenableState; } @@ -9298,6 +9316,8 @@ function finishHooks(Component, props, children, refOrContext) { // restarting until no more updates are scheduled. didScheduleRenderPhaseUpdate = false; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; numberOfReRenders += 1; // Start over from the beginning of the list @@ -9319,6 +9339,18 @@ function checkDidRenderIdHook() { // separate function to avoid using an array tuple. var didRenderIdHook = localIdCounter !== 0; return didRenderIdHook; +} +function getFormStateCount() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateCounter; +} +function getFormStateMatchingIndex() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateMatchingIndex; } // Reset the internal hooks state if an error occurs while rendering a component function resetHooksState() { @@ -9608,7 +9640,11 @@ function useOptimistic(passthrough, reducer) { } function useFormState(action, initialState, permalink) { - resolveCurrentlyRenderingComponent(); // Bind the initial state to the first argument of the action. + resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. + // TODO: We should also track which hook matches the form state passed at + // the root, if any. Matching is not yet implemented. + + formStateCounter++; // Bind the initial state to the first argument of the action. // TODO: Use the keypath (or permalink) to check if there's matching state // from the previous page. @@ -10400,6 +10436,8 @@ function renderIndeterminateComponent( legacyContext ); var hasId = checkDidRenderIdHook(); + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); { // Support for module components is deprecated and is removed behind a flag. @@ -10432,30 +10470,79 @@ function renderIndeterminateComponent( { { validateFunctionComponentInDev(Component); - } // We're now successfully past this task, and we don't have to pop back to - // the previous task every again, so we can use the destructive recursive form. - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, value, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - - task.treeContext = prevTreeContext; - } else { - renderNodeDestructive(request, task, null, value, 0); } + + finishFunctionComponent( + request, + task, + value, + hasId, + formStateCount, + formStateMatchingIndex + ); } popComponentStackInDEV(task); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = false; + + if (formStateCount !== 0) { + // For each useFormState hook, emit a marker that indicates whether we + // rendered using the form state passed at the root. + // TODO: As an optimization, Fizz should only emit these markers if form + // state is passed at the root. + var segment = task.blockedSegment; + + if (segment === null); + else { + didEmitFormStateMarkers = true; + var target = segment.chunks; + + for (var i = 0; i < formStateCount; i++) { + if (i === formStateMatchingIndex) { + pushFormStateMarkerIsMatching(target); + } else { + pushFormStateMarkerIsNotMatching(target); + } + } + } + } + + if (hasId) { + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + var prevTreeContext = task.treeContext; + var totalChildren = 1; + var index = 0; // Modify the id context. Because we'll need to reset this if something + // suspends or errors, we'll use the non-destructive render path. + + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block + // because renderNode takes care of unwinding the stack. + + task.treeContext = prevTreeContext; + } else if (didEmitFormStateMarkers) { + // If there were formState hooks, we must use the non-destructive path + // because this component is not a pure indirection; we emitted markers + // to the stream. + renderNode(request, task, children, 0); + } else { + // We're now successfully past this task, and we haven't modified the + // context stack. We don't have to pop back to the previous task every + // again, so we can use the destructive recursive form. + renderNodeDestructive(request, task, null, children, 0); + } +} + function validateFunctionComponentInDev(Component) { { if (Component) { @@ -10541,22 +10628,16 @@ function renderForwardRef(request, task, prevThenableState, type, props, ref) { ref ); var hasId = checkDidRenderIdHook(); - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - } else { - renderNodeDestructive(request, task, null, children, 0); - } - + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); + finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex + ); popComponentStackInDEV(task); } diff --git a/compiled/facebook-www/ReactDOMServer-dev.modern.js b/compiled/facebook-www/ReactDOMServer-dev.modern.js index 09c116641630c..7e9b6b3fe2ab2 100644 --- a/compiled/facebook-www/ReactDOMServer-dev.modern.js +++ b/compiled/facebook-www/ReactDOMServer-dev.modern.js @@ -19,7 +19,7 @@ if (__DEV__) { var React = require("react"); var ReactDOM = require("react-dom"); -var ReactVersion = "18.3.0-www-modern-4a509011"; +var ReactVersion = "18.3.0-www-modern-d8aac565"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -3153,6 +3153,15 @@ function pushStartOption(target, props, formatContext) { return children; } +var formStateMarkerIsMatching = stringToPrecomputedChunk(""); +var formStateMarkerIsNotMatching = stringToPrecomputedChunk(""); +function pushFormStateMarkerIsMatching(target) { + target.push(formStateMarkerIsMatching); +} +function pushFormStateMarkerIsNotMatching(target) { + target.push(formStateMarkerIsNotMatching); +} + function pushStartForm(target, props, resumableState, renderState) { target.push(startChunkForTag("form")); var children = null; @@ -8911,7 +8920,14 @@ var isReRender = false; // Whether an update was scheduled during the currently var didScheduleRenderPhaseUpdate = false; // Counts the number of useId hooks in this component -var localIdCounter = 0; // Counts the number of use(thenable) calls in this component +var localIdCounter = 0; // Chunks that should be pushed to the stream once the component +// finishes rendering. +// Counts the number of useFormState calls in this component + +var formStateCounter = 0; // The index of the useFormState hook that matches the one passed in at the +// root during an MPA navigation, if any. + +var formStateMatchingIndex = -1; // Counts the number of use(thenable) calls in this component var thenableIndexCounter = 0; var thenableState = null; // Lazily created map of render-phase updates @@ -9044,6 +9060,8 @@ function prepareToUseHooks(task, componentIdentity, prevThenableState) { // workInProgressHook = null; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; thenableState = prevThenableState; } @@ -9057,6 +9075,8 @@ function finishHooks(Component, props, children, refOrContext) { // restarting until no more updates are scheduled. didScheduleRenderPhaseUpdate = false; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; numberOfReRenders += 1; // Start over from the beginning of the list @@ -9078,6 +9098,18 @@ function checkDidRenderIdHook() { // separate function to avoid using an array tuple. var didRenderIdHook = localIdCounter !== 0; return didRenderIdHook; +} +function getFormStateCount() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateCounter; +} +function getFormStateMatchingIndex() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateMatchingIndex; } // Reset the internal hooks state if an error occurs while rendering a component function resetHooksState() { @@ -9367,7 +9399,11 @@ function useOptimistic(passthrough, reducer) { } function useFormState(action, initialState, permalink) { - resolveCurrentlyRenderingComponent(); // Bind the initial state to the first argument of the action. + resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. + // TODO: We should also track which hook matches the form state passed at + // the root, if any. Matching is not yet implemented. + + formStateCounter++; // Bind the initial state to the first argument of the action. // TODO: Use the keypath (or permalink) to check if there's matching state // from the previous page. @@ -10137,6 +10173,8 @@ function renderIndeterminateComponent( legacyContext ); var hasId = checkDidRenderIdHook(); + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); { // Support for module components is deprecated and is removed behind a flag. @@ -10180,30 +10218,79 @@ function renderIndeterminateComponent( { validateFunctionComponentInDev(Component); - } // We're now successfully past this task, and we don't have to pop back to - // the previous task every again, so we can use the destructive recursive form. - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, value, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - - task.treeContext = prevTreeContext; - } else { - renderNodeDestructive(request, task, null, value, 0); } + + finishFunctionComponent( + request, + task, + value, + hasId, + formStateCount, + formStateMatchingIndex + ); } popComponentStackInDEV(task); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = false; + + if (formStateCount !== 0) { + // For each useFormState hook, emit a marker that indicates whether we + // rendered using the form state passed at the root. + // TODO: As an optimization, Fizz should only emit these markers if form + // state is passed at the root. + var segment = task.blockedSegment; + + if (segment === null); + else { + didEmitFormStateMarkers = true; + var target = segment.chunks; + + for (var i = 0; i < formStateCount; i++) { + if (i === formStateMatchingIndex) { + pushFormStateMarkerIsMatching(target); + } else { + pushFormStateMarkerIsNotMatching(target); + } + } + } + } + + if (hasId) { + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + var prevTreeContext = task.treeContext; + var totalChildren = 1; + var index = 0; // Modify the id context. Because we'll need to reset this if something + // suspends or errors, we'll use the non-destructive render path. + + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block + // because renderNode takes care of unwinding the stack. + + task.treeContext = prevTreeContext; + } else if (didEmitFormStateMarkers) { + // If there were formState hooks, we must use the non-destructive path + // because this component is not a pure indirection; we emitted markers + // to the stream. + renderNode(request, task, children, 0); + } else { + // We're now successfully past this task, and we haven't modified the + // context stack. We don't have to pop back to the previous task every + // again, so we can use the destructive recursive form. + renderNodeDestructive(request, task, null, children, 0); + } +} + function validateFunctionComponentInDev(Component) { { if (Component) { @@ -10289,22 +10376,16 @@ function renderForwardRef(request, task, prevThenableState, type, props, ref) { ref ); var hasId = checkDidRenderIdHook(); - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - } else { - renderNodeDestructive(request, task, null, children, 0); - } - + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); + finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex + ); popComponentStackInDEV(task); } diff --git a/compiled/facebook-www/ReactDOMServer-prod.classic.js b/compiled/facebook-www/ReactDOMServer-prod.classic.js index 69e541e484454..a4fe198adfc8c 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.classic.js +++ b/compiled/facebook-www/ReactDOMServer-prod.classic.js @@ -2641,6 +2641,8 @@ var objectIs = "function" === typeof Object.is ? Object.is : is, isReRender = !1, didScheduleRenderPhaseUpdate = !1, localIdCounter = 0, + formStateCounter = 0, + formStateMatchingIndex = -1, thenableIndexCounter = 0, thenableState = null, renderPhaseUpdates = null, @@ -2669,7 +2671,9 @@ function createWorkInProgressHook() { function finishHooks(Component, props, children, refOrContext) { for (; didScheduleRenderPhaseUpdate; ) (didScheduleRenderPhaseUpdate = !1), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (numberOfReRenders += 1), (workInProgressHook = null), (children = Component(props, refOrContext)); @@ -2785,6 +2789,7 @@ function useFormState(action, initialState, permalink) { boundAction(payload); } resolveCurrentlyRenderingComponent(); + formStateCounter++; var boundAction = action.bind(null, initialState); "function" === typeof boundAction.$$FORM_ACTION && (dispatch.$$FORM_ACTION = function (prefix) { @@ -3054,6 +3059,35 @@ function fatalError(request, error) { ? ((request.status = 2), request.destination.destroy(error)) : ((request.status = 1), (request.fatalError = error)); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = !1; + if (0 !== formStateCount) { + var segment = task.blockedSegment; + if (null !== segment) { + didEmitFormStateMarkers = !0; + segment = segment.chunks; + for (var i = 0; i < formStateCount; i++) + i === formStateMatchingIndex + ? segment.push("\x3c!--F!--\x3e") + : segment.push("\x3c!--F--\x3e"); + } + } + hasId + ? ((hasId = task.treeContext), + (task.treeContext = pushTreeContext(hasId, 1, 0)), + renderNode(request, task, children, 0), + (task.treeContext = hasId)) + : didEmitFormStateMarkers + ? renderNode(request, task, children, 0) + : renderNodeDestructiveImpl(request, task, null, children, 0); +} function resolveDefaultProps(Component, baseProps) { if (Component && Component.defaultProps) { baseProps = assign({}, baseProps); @@ -3181,7 +3215,9 @@ function renderElement(request, task, prevThenableState, type, props, ref) { (contextKey = getMaskedContext(type, task.legacyContext)), (currentlyRenderingComponent = {}), (currentlyRenderingTask = task), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (thenableState = prevThenableState), (JSCompiler_inline_result = type(props, contextKey)), (props = finishHooks( @@ -3190,12 +3226,14 @@ function renderElement(request, task, prevThenableState, type, props, ref) { JSCompiler_inline_result, contextKey )), - 0 !== localIdCounter - ? ((type = task.treeContext), - (task.treeContext = pushTreeContext(type, 1, 0)), - renderNode(request, task, props, 0), - (task.treeContext = type)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); else if ("string" === typeof type) { contextKey = task.blockedSegment; prevThenableState = pushStartInstance( @@ -3365,14 +3403,20 @@ function renderElement(request, task, prevThenableState, type, props, ref) { type = type.render; currentlyRenderingComponent = {}; currentlyRenderingTask = task; - thenableIndexCounter = localIdCounter = 0; + formStateCounter = localIdCounter = 0; + formStateMatchingIndex = -1; + thenableIndexCounter = 0; thenableState = prevThenableState; contextKey = type(props, ref); props = finishHooks(type, props, contextKey, ref); - 0 !== localIdCounter - ? ((task.treeContext = pushTreeContext(task.treeContext, 1, 0)), - renderNode(request, task, props, 0)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); return; case REACT_MEMO_TYPE: type = type.type; @@ -4434,4 +4478,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "18.3.0-www-classic-25483e90"; +exports.version = "18.3.0-www-classic-9c45eb83"; diff --git a/compiled/facebook-www/ReactDOMServer-prod.modern.js b/compiled/facebook-www/ReactDOMServer-prod.modern.js index ca9df17cfed79..b2242d4633ff6 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServer-prod.modern.js @@ -2633,6 +2633,8 @@ var objectIs = "function" === typeof Object.is ? Object.is : is, isReRender = !1, didScheduleRenderPhaseUpdate = !1, localIdCounter = 0, + formStateCounter = 0, + formStateMatchingIndex = -1, thenableIndexCounter = 0, thenableState = null, renderPhaseUpdates = null, @@ -2661,7 +2663,9 @@ function createWorkInProgressHook() { function finishHooks(Component, props, children, refOrContext) { for (; didScheduleRenderPhaseUpdate; ) (didScheduleRenderPhaseUpdate = !1), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (numberOfReRenders += 1), (workInProgressHook = null), (children = Component(props, refOrContext)); @@ -2777,6 +2781,7 @@ function useFormState(action, initialState, permalink) { boundAction(payload); } resolveCurrentlyRenderingComponent(); + formStateCounter++; var boundAction = action.bind(null, initialState); "function" === typeof boundAction.$$FORM_ACTION && (dispatch.$$FORM_ACTION = function (prefix) { @@ -3046,6 +3051,35 @@ function fatalError(request, error) { ? ((request.status = 2), request.destination.destroy(error)) : ((request.status = 1), (request.fatalError = error)); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = !1; + if (0 !== formStateCount) { + var segment = task.blockedSegment; + if (null !== segment) { + didEmitFormStateMarkers = !0; + segment = segment.chunks; + for (var i = 0; i < formStateCount; i++) + i === formStateMatchingIndex + ? segment.push("\x3c!--F!--\x3e") + : segment.push("\x3c!--F--\x3e"); + } + } + hasId + ? ((hasId = task.treeContext), + (task.treeContext = pushTreeContext(hasId, 1, 0)), + renderNode(request, task, children, 0), + (task.treeContext = hasId)) + : didEmitFormStateMarkers + ? renderNode(request, task, children, 0) + : renderNodeDestructiveImpl(request, task, null, children, 0); +} function resolveDefaultProps(Component, baseProps) { if (Component && Component.defaultProps) { baseProps = assign({}, baseProps); @@ -3158,16 +3192,20 @@ function renderElement(request, task, prevThenableState, type, props, ref) { } else (currentlyRenderingComponent = {}), (currentlyRenderingTask = task), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (thenableState = prevThenableState), (JSCompiler_inline_result = type(props, void 0)), (props = finishHooks(type, props, JSCompiler_inline_result, void 0)), - 0 !== localIdCounter - ? ((type = task.treeContext), - (task.treeContext = pushTreeContext(type, 1, 0)), - renderNode(request, task, props, 0), - (task.treeContext = type)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); else if ("string" === typeof type) { JSCompiler_inline_result = task.blockedSegment; ref = pushStartInstance( @@ -3333,14 +3371,20 @@ function renderElement(request, task, prevThenableState, type, props, ref) { type = type.render; currentlyRenderingComponent = {}; currentlyRenderingTask = task; - thenableIndexCounter = localIdCounter = 0; + formStateCounter = localIdCounter = 0; + formStateMatchingIndex = -1; + thenableIndexCounter = 0; thenableState = prevThenableState; JSCompiler_inline_result = type(props, ref); props = finishHooks(type, props, JSCompiler_inline_result, ref); - 0 !== localIdCounter - ? ((task.treeContext = pushTreeContext(task.treeContext, 1, 0)), - renderNode(request, task, props, 0)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); return; case REACT_MEMO_TYPE: type = type.type; @@ -4408,4 +4452,4 @@ exports.renderToString = function (children, options) { 'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server' ); }; -exports.version = "18.3.0-www-modern-9a25d12d"; +exports.version = "18.3.0-www-modern-987ee6d1"; diff --git a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js index 3706a2921c123..7e16c4d95017a 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js @@ -3145,6 +3145,15 @@ function pushStartOption(target, props, formatContext) { return children; } +var formStateMarkerIsMatching = stringToPrecomputedChunk(""); +var formStateMarkerIsNotMatching = stringToPrecomputedChunk(""); +function pushFormStateMarkerIsMatching(target) { + target.push(formStateMarkerIsMatching); +} +function pushFormStateMarkerIsNotMatching(target) { + target.push(formStateMarkerIsNotMatching); +} + function pushStartForm(target, props, resumableState, renderState) { target.push(startChunkForTag("form")); var children = null; @@ -8817,7 +8826,14 @@ var isReRender = false; // Whether an update was scheduled during the currently var didScheduleRenderPhaseUpdate = false; // Counts the number of useId hooks in this component -var localIdCounter = 0; // Counts the number of use(thenable) calls in this component +var localIdCounter = 0; // Chunks that should be pushed to the stream once the component +// finishes rendering. +// Counts the number of useFormState calls in this component + +var formStateCounter = 0; // The index of the useFormState hook that matches the one passed in at the +// root during an MPA navigation, if any. + +var formStateMatchingIndex = -1; // Counts the number of use(thenable) calls in this component var thenableIndexCounter = 0; var thenableState = null; // Lazily created map of render-phase updates @@ -8950,6 +8966,8 @@ function prepareToUseHooks(task, componentIdentity, prevThenableState) { // workInProgressHook = null; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; thenableState = prevThenableState; } @@ -8963,6 +8981,8 @@ function finishHooks(Component, props, children, refOrContext) { // restarting until no more updates are scheduled. didScheduleRenderPhaseUpdate = false; localIdCounter = 0; + formStateCounter = 0; + formStateMatchingIndex = -1; thenableIndexCounter = 0; numberOfReRenders += 1; // Start over from the beginning of the list @@ -8984,6 +9004,18 @@ function checkDidRenderIdHook() { // separate function to avoid using an array tuple. var didRenderIdHook = localIdCounter !== 0; return didRenderIdHook; +} +function getFormStateCount() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateCounter; +} +function getFormStateMatchingIndex() { + // This should be called immediately after every finishHooks call. + // Conceptually, it's part of the return value of finishHooks; it's only a + // separate function to avoid using an array tuple. + return formStateMatchingIndex; } // Reset the internal hooks state if an error occurs while rendering a component function resetHooksState() { @@ -9273,7 +9305,11 @@ function useOptimistic(passthrough, reducer) { } function useFormState(action, initialState, permalink) { - resolveCurrentlyRenderingComponent(); // Bind the initial state to the first argument of the action. + resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. + // TODO: We should also track which hook matches the form state passed at + // the root, if any. Matching is not yet implemented. + + formStateCounter++; // Bind the initial state to the first argument of the action. // TODO: Use the keypath (or permalink) to check if there's matching state // from the previous page. @@ -10040,6 +10076,8 @@ function renderIndeterminateComponent( legacyContext ); var hasId = checkDidRenderIdHook(); + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); { // Support for module components is deprecated and is removed behind a flag. @@ -10083,30 +10121,79 @@ function renderIndeterminateComponent( { validateFunctionComponentInDev(Component); - } // We're now successfully past this task, and we don't have to pop back to - // the previous task every again, so we can use the destructive recursive form. - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, value, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - - task.treeContext = prevTreeContext; - } else { - renderNodeDestructive(request, task, null, value, 0); } + + finishFunctionComponent( + request, + task, + value, + hasId, + formStateCount, + formStateMatchingIndex + ); } popComponentStackInDEV(task); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = false; + + if (formStateCount !== 0) { + // For each useFormState hook, emit a marker that indicates whether we + // rendered using the form state passed at the root. + // TODO: As an optimization, Fizz should only emit these markers if form + // state is passed at the root. + var segment = task.blockedSegment; + + if (segment === null); + else { + didEmitFormStateMarkers = true; + var target = segment.chunks; + + for (var i = 0; i < formStateCount; i++) { + if (i === formStateMatchingIndex) { + pushFormStateMarkerIsMatching(target); + } else { + pushFormStateMarkerIsNotMatching(target); + } + } + } + } + + if (hasId) { + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + var prevTreeContext = task.treeContext; + var totalChildren = 1; + var index = 0; // Modify the id context. Because we'll need to reset this if something + // suspends or errors, we'll use the non-destructive render path. + + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block + // because renderNode takes care of unwinding the stack. + + task.treeContext = prevTreeContext; + } else if (didEmitFormStateMarkers) { + // If there were formState hooks, we must use the non-destructive path + // because this component is not a pure indirection; we emitted markers + // to the stream. + renderNode(request, task, children, 0); + } else { + // We're now successfully past this task, and we haven't modified the + // context stack. We don't have to pop back to the previous task every + // again, so we can use the destructive recursive form. + renderNodeDestructive(request, task, null, children, 0); + } +} + function validateFunctionComponentInDev(Component) { { if (Component) { @@ -10192,22 +10279,16 @@ function renderForwardRef(request, task, prevThenableState, type, props, ref) { ref ); var hasId = checkDidRenderIdHook(); - - if (hasId) { - // This component materialized an id. We treat this as its own level, with - // a single "child" slot. - var prevTreeContext = task.treeContext; - var totalChildren = 1; - var index = 0; // Modify the id context. Because we'll need to reset this if something - // suspends or errors, we'll use the non-destructive render path. - - task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); - renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block - // because renderNode takes care of unwinding the stack. - } else { - renderNodeDestructive(request, task, null, children, 0); - } - + var formStateCount = getFormStateCount(); + var formStateMatchingIndex = getFormStateMatchingIndex(); + finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex + ); popComponentStackInDEV(task); } diff --git a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js index 399b67d7e1313..4cbe5ee96d6a4 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js @@ -2481,6 +2481,8 @@ var objectIs = "function" === typeof Object.is ? Object.is : is, isReRender = !1, didScheduleRenderPhaseUpdate = !1, localIdCounter = 0, + formStateCounter = 0, + formStateMatchingIndex = -1, thenableIndexCounter = 0, thenableState = null, renderPhaseUpdates = null, @@ -2512,7 +2514,9 @@ function createWorkInProgressHook() { function finishHooks(Component, props, children, refOrContext) { for (; didScheduleRenderPhaseUpdate; ) (didScheduleRenderPhaseUpdate = !1), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (numberOfReRenders += 1), (workInProgressHook = null), (children = Component(props, refOrContext)); @@ -2633,6 +2637,7 @@ function useFormState(action, initialState, permalink) { boundAction(payload); } resolveCurrentlyRenderingComponent(); + formStateCounter++; var boundAction = action.bind(null, initialState); "function" === typeof boundAction.$$FORM_ACTION && (dispatch.$$FORM_ACTION = function (prefix) { @@ -2843,6 +2848,35 @@ function fatalError(request, error) { (request.error = error)) : ((request.status = 1), (request.fatalError = error)); } +function finishFunctionComponent( + request, + task, + children, + hasId, + formStateCount, + formStateMatchingIndex +) { + var didEmitFormStateMarkers = !1; + if (0 !== formStateCount) { + var segment = task.blockedSegment; + if (null !== segment) { + didEmitFormStateMarkers = !0; + segment = segment.chunks; + for (var i = 0; i < formStateCount; i++) + i === formStateMatchingIndex + ? segment.push("\x3c!--F!--\x3e") + : segment.push("\x3c!--F--\x3e"); + } + } + hasId + ? ((hasId = task.treeContext), + (task.treeContext = pushTreeContext(hasId, 1, 0)), + renderNode(request, task, children, 0), + (task.treeContext = hasId)) + : didEmitFormStateMarkers + ? renderNode(request, task, children, 0) + : renderNodeDestructiveImpl(request, task, null, children, 0); +} function resolveDefaultProps(Component, baseProps) { if (Component && Component.defaultProps) { baseProps = assign({}, baseProps); @@ -2955,16 +2989,20 @@ function renderElement(request, task, prevThenableState, type, props, ref) { } else (currentlyRenderingComponent = {}), (currentlyRenderingTask = task), - (thenableIndexCounter = localIdCounter = 0), + (formStateCounter = localIdCounter = 0), + (formStateMatchingIndex = -1), + (thenableIndexCounter = 0), (thenableState = prevThenableState), (JSCompiler_inline_result = type(props, void 0)), (props = finishHooks(type, props, JSCompiler_inline_result, void 0)), - 0 !== localIdCounter - ? ((type = task.treeContext), - (task.treeContext = pushTreeContext(type, 1, 0)), - renderNode(request, task, props, 0), - (task.treeContext = type)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); else if ("string" === typeof type) { JSCompiler_inline_result = task.blockedSegment; ref = pushStartInstance( @@ -3129,14 +3167,20 @@ function renderElement(request, task, prevThenableState, type, props, ref) { type = type.render; currentlyRenderingComponent = {}; currentlyRenderingTask = task; - thenableIndexCounter = localIdCounter = 0; + formStateCounter = localIdCounter = 0; + formStateMatchingIndex = -1; + thenableIndexCounter = 0; thenableState = prevThenableState; JSCompiler_inline_result = type(props, ref); props = finishHooks(type, props, JSCompiler_inline_result, ref); - 0 !== localIdCounter - ? ((task.treeContext = pushTreeContext(task.treeContext, 1, 0)), - renderNode(request, task, props, 0)) - : renderNodeDestructiveImpl(request, task, null, props, 0); + finishFunctionComponent( + request, + task, + props, + 0 !== localIdCounter, + formStateCounter, + formStateMatchingIndex + ); return; case REACT_MEMO_TYPE: type = type.type; diff --git a/compiled/facebook-www/ReactDOMTesting-dev.classic.js b/compiled/facebook-www/ReactDOMTesting-dev.classic.js index 22cfe4ad4a29f..d6ae5210071cb 100644 --- a/compiled/facebook-www/ReactDOMTesting-dev.classic.js +++ b/compiled/facebook-www/ReactDOMTesting-dev.classic.js @@ -148,6 +148,7 @@ var enableHostSingletons = true; var enableClientRenderFallbackOnTextMismatch = false; var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation. +var enableFormActions = false; var enableSuspenseCallback = true; var FunctionComponent = 0; @@ -34625,7 +34626,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-classic-8046a020"; +var ReactVersion = "18.3.0-www-classic-d80c0530"; function createPortal$1( children, @@ -43555,7 +43556,8 @@ function getNextHydratable(node) { if ( nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || - nodeData === SUSPENSE_PENDING_START_DATA + nodeData === SUSPENSE_PENDING_START_DATA || + enableFormActions ) { break; } diff --git a/compiled/facebook-www/ReactDOMTesting-dev.modern.js b/compiled/facebook-www/ReactDOMTesting-dev.modern.js index 1d501db0a7fc8..7c3a004190727 100644 --- a/compiled/facebook-www/ReactDOMTesting-dev.modern.js +++ b/compiled/facebook-www/ReactDOMTesting-dev.modern.js @@ -134,6 +134,7 @@ var enableHostSingletons = true; var enableClientRenderFallbackOnTextMismatch = false; var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation. +var enableFormActions = false; var enableSuspenseCallback = true; var ReactSharedInternals = @@ -34470,7 +34471,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-modern-7002e203"; +var ReactVersion = "18.3.0-www-modern-5c6a0c8b"; function createPortal$1( children, @@ -44065,7 +44066,8 @@ function getNextHydratable(node) { if ( nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || - nodeData === SUSPENSE_PENDING_START_DATA + nodeData === SUSPENSE_PENDING_START_DATA || + enableFormActions ) { break; } diff --git a/compiled/facebook-www/ReactTestRenderer-dev.classic.js b/compiled/facebook-www/ReactTestRenderer-dev.classic.js index 2a2246c29f3a6..56439b2ab2286 100644 --- a/compiled/facebook-www/ReactTestRenderer-dev.classic.js +++ b/compiled/facebook-www/ReactTestRenderer-dev.classic.js @@ -24356,7 +24356,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-classic-8046a020"; +var ReactVersion = "18.3.0-www-classic-d80c0530"; // Might add PROFILE later. diff --git a/compiled/facebook-www/ReactTestRenderer-dev.modern.js b/compiled/facebook-www/ReactTestRenderer-dev.modern.js index 56653e97bbc06..4e94c76f98eba 100644 --- a/compiled/facebook-www/ReactTestRenderer-dev.modern.js +++ b/compiled/facebook-www/ReactTestRenderer-dev.modern.js @@ -24356,7 +24356,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-modern-25733132"; +var ReactVersion = "18.3.0-www-modern-fe8cfdcc"; // Might add PROFILE later.