From 97bcd7f29047dac85dc3a0f98bb774974dbad6c1 Mon Sep 17 00:00:00 2001 From: acdlite Date: Thu, 14 Sep 2023 05:11:47 +0000 Subject: [PATCH] useFormState: MPA submissions to a different page (#27372) The permalink option of useFormState controls which page the form is submitted to during an MPA form submission (i.e. a submission that happens before hydration, or when JS is disabled). If the same useFormState appears on the resulting page, and the permalink option matches, it should receive the form state from the submission despite the fact that the keypaths do not match. So the logic for whether a form state instance is considered a match is: - Both instances must be passed the same action signature - If a permalink is provided, the permalinks must match. - If a permalink is not provided, the keypaths must match. Currently, if there are multiple matching useFormStates, they will all match and receive the form state. We should probably only match the first one, and/or warn when this happens. I've left this as a TODO for now, pending further discussion. DiffTrain build for [caa716d50bdeef3a1ac5e3e0cfcc14f4d91f2028](https://github.com/facebook/react/commit/caa716d50bdeef3a1ac5e3e0cfcc14f4d91f2028) --- compiled/facebook-www/REVISION | 2 +- .../ReactDOMServer-dev.classic.js | 67 ++++++++++++++----- .../facebook-www/ReactDOMServer-dev.modern.js | 67 ++++++++++++++----- .../ReactDOMServer-prod.classic.js | 41 +++++++----- .../ReactDOMServer-prod.modern.js | 41 +++++++----- .../ReactDOMServerStreaming-dev.modern.js | 65 +++++++++++++----- .../ReactDOMServerStreaming-prod.modern.js | 39 ++++++----- .../ReactTestRenderer-dev.modern.js | 2 +- 8 files changed, 216 insertions(+), 108 deletions(-) diff --git a/compiled/facebook-www/REVISION b/compiled/facebook-www/REVISION index 07e32e9318ac1..6aa0ca1ab295d 100644 --- a/compiled/facebook-www/REVISION +++ b/compiled/facebook-www/REVISION @@ -1 +1 @@ -a6e4791b11816374d015eb4531a82e6cf209c7f2 +caa716d50bdeef3a1ac5e3e0cfcc14f4d91f2028 diff --git a/compiled/facebook-www/ReactDOMServer-dev.classic.js b/compiled/facebook-www/ReactDOMServer-dev.classic.js index 3cc3e12497752..e438fc10cf081 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-f2b91051"; +var ReactVersion = "18.3.0-www-classic-360a070f"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -9427,6 +9427,16 @@ function useOptimistic(passthrough, reducer) { return [passthrough, unsupportedSetOptimisticState]; } +function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) { + if (permalink !== undefined) { + return "p" + permalink; + } else { + // Append a node to the key path that represents the form state hook. + var keyPath = [componentKeyPath, null, hookIndex]; + return "k" + JSON.stringify(keyPath); + } +} + function useFormState(action, initialState, permalink) { resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to // track the position of this useFormState hook relative to the other ones in @@ -9440,32 +9450,43 @@ function useFormState(action, initialState, permalink) { if (typeof formAction === "function") { // This is a server action. These have additional features to enable // MPA-style form submissions with progressive enhancement. - // Determine the current form state. If we received state during an MPA form + // TODO: If the same permalink is passed to multiple useFormStates, and + // they all have the same action signature, Fizz will pass the postback + // state to all of them. We should probably only pass it to the first one, + // and/or warn. + // The key is lazily generated and deduped so the that the keypath doesn't + // get JSON.stringify-ed unnecessarily, and at most once. + var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form // submission, then we will reuse that, if the action identity matches. // Otherwise we'll use the initial state argument. We will emit a comment // marker into the stream that indicates whether the state was reused. - var state = initialState; // Append a node to the key path that represents the form state hook. - var componentKey = currentlyRenderingKeyPath; - var key = [componentKey, null, formStateHookIndex]; - var keyJSON = JSON.stringify(key); + var state = initialState; + var componentKeyPath = currentlyRenderingKeyPath; var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing] var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (postbackFormState !== null && typeof isSignatureEqual === "function") { - var postbackKeyJSON = postbackFormState[1]; + var postbackKey = postbackFormState[1]; var postbackReferenceId = postbackFormState[2]; var postbackBoundArity = postbackFormState[3]; if ( - postbackKeyJSON === keyJSON && isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity) ) { - // This was a match - formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); - state = postbackFormState[0]; + if (postbackKey === nextPostbackStateKey) { + // This was a match + formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + + state = postbackFormState[0]; + } } } // Bind the state to the first argument of the action. @@ -9478,19 +9499,29 @@ function useFormState(action, initialState, permalink) { if (typeof boundAction.$$FORM_ACTION === "function") { // $FlowIgnore[prop-missing] dispatch.$$FORM_ACTION = function (prefix) { - var metadata = boundAction.$$FORM_ACTION(prefix); - var formData = metadata.data; - - if (formData) { - formData.append("$ACTION_KEY", keyJSON); - } // Override the action URL + var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL if (permalink !== undefined) { { checkAttributeStringCoercion(permalink, "target"); } - metadata.action = permalink + ""; + permalink += ""; + metadata.action = permalink; + } + + var formData = metadata.data; + + if (formData) { + if (nextPostbackStateKey === null) { + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); + } + + formData.append("$ACTION_KEY", nextPostbackStateKey); } return metadata; diff --git a/compiled/facebook-www/ReactDOMServer-dev.modern.js b/compiled/facebook-www/ReactDOMServer-dev.modern.js index bc12f948c5356..a490c8426a480 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-82925a4c"; +var ReactVersion = "18.3.0-www-modern-b814d906"; // This refers to a WWW module. var warningWWW = require("warning"); @@ -9186,6 +9186,16 @@ function useOptimistic(passthrough, reducer) { return [passthrough, unsupportedSetOptimisticState]; } +function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) { + if (permalink !== undefined) { + return "p" + permalink; + } else { + // Append a node to the key path that represents the form state hook. + var keyPath = [componentKeyPath, null, hookIndex]; + return "k" + JSON.stringify(keyPath); + } +} + function useFormState(action, initialState, permalink) { resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to // track the position of this useFormState hook relative to the other ones in @@ -9199,32 +9209,43 @@ function useFormState(action, initialState, permalink) { if (typeof formAction === "function") { // This is a server action. These have additional features to enable // MPA-style form submissions with progressive enhancement. - // Determine the current form state. If we received state during an MPA form + // TODO: If the same permalink is passed to multiple useFormStates, and + // they all have the same action signature, Fizz will pass the postback + // state to all of them. We should probably only pass it to the first one, + // and/or warn. + // The key is lazily generated and deduped so the that the keypath doesn't + // get JSON.stringify-ed unnecessarily, and at most once. + var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form // submission, then we will reuse that, if the action identity matches. // Otherwise we'll use the initial state argument. We will emit a comment // marker into the stream that indicates whether the state was reused. - var state = initialState; // Append a node to the key path that represents the form state hook. - var componentKey = currentlyRenderingKeyPath; - var key = [componentKey, null, formStateHookIndex]; - var keyJSON = JSON.stringify(key); + var state = initialState; + var componentKeyPath = currentlyRenderingKeyPath; var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing] var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (postbackFormState !== null && typeof isSignatureEqual === "function") { - var postbackKeyJSON = postbackFormState[1]; + var postbackKey = postbackFormState[1]; var postbackReferenceId = postbackFormState[2]; var postbackBoundArity = postbackFormState[3]; if ( - postbackKeyJSON === keyJSON && isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity) ) { - // This was a match - formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); - state = postbackFormState[0]; + if (postbackKey === nextPostbackStateKey) { + // This was a match + formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + + state = postbackFormState[0]; + } } } // Bind the state to the first argument of the action. @@ -9237,19 +9258,29 @@ function useFormState(action, initialState, permalink) { if (typeof boundAction.$$FORM_ACTION === "function") { // $FlowIgnore[prop-missing] dispatch.$$FORM_ACTION = function (prefix) { - var metadata = boundAction.$$FORM_ACTION(prefix); - var formData = metadata.data; - - if (formData) { - formData.append("$ACTION_KEY", keyJSON); - } // Override the action URL + var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL if (permalink !== undefined) { { checkAttributeStringCoercion(permalink, "target"); } - metadata.action = permalink + ""; + permalink += ""; + metadata.action = permalink; + } + + var formData = metadata.data; + + if (formData) { + if (nextPostbackStateKey === null) { + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); + } + + formData.append("$ACTION_KEY", nextPostbackStateKey); } return metadata; diff --git a/compiled/facebook-www/ReactDOMServer-prod.classic.js b/compiled/facebook-www/ReactDOMServer-prod.classic.js index d171b18ab5837..8c30155eb5bdd 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.classic.js +++ b/compiled/facebook-www/ReactDOMServer-prod.classic.js @@ -2742,24 +2742,21 @@ function useFormState(action, initialState, permalink) { var formStateHookIndex = formStateCounter++, request = currentlyRenderingRequest; if ("function" === typeof action.$$FORM_ACTION) { - var keyJSON = JSON.stringify([ - currentlyRenderingKeyPath, - null, - formStateHookIndex - ]); + var nextPostbackStateKey = null, + componentKeyPath = currentlyRenderingKeyPath; request = request.formState; var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (null !== request && "function" === typeof isSignatureEqual) { - var postbackReferenceId = request[2], - postbackBoundArity = request[3]; - request[1] === keyJSON && - isSignatureEqual.call( - action, - postbackReferenceId, - postbackBoundArity - ) && - ((formStateMatchingIndex = formStateHookIndex), - (initialState = request[0])); + var postbackKey = request[1]; + isSignatureEqual.call(action, request[2], request[3]) && + ((nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + postbackKey === nextPostbackStateKey && + ((formStateMatchingIndex = formStateHookIndex), + (initialState = request[0]))); } var boundAction = action.bind(null, initialState); action = function (payload) { @@ -2768,9 +2765,17 @@ function useFormState(action, initialState, permalink) { "function" === typeof boundAction.$$FORM_ACTION && (action.$$FORM_ACTION = function (prefix) { prefix = boundAction.$$FORM_ACTION(prefix); + void 0 !== permalink && + ((permalink += ""), (prefix.action = permalink)); var formData = prefix.data; - formData && formData.append("$ACTION_KEY", keyJSON); - void 0 !== permalink && (prefix.action = permalink + ""); + formData && + (null === nextPostbackStateKey && + (nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + formData.append("$ACTION_KEY", nextPostbackStateKey)); return prefix; }); return [initialState, action]; @@ -4551,4 +4556,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-f7fe51fc"; +exports.version = "18.3.0-www-classic-f04a97ef"; diff --git a/compiled/facebook-www/ReactDOMServer-prod.modern.js b/compiled/facebook-www/ReactDOMServer-prod.modern.js index 886df1fe88325..c91401559ef58 100644 --- a/compiled/facebook-www/ReactDOMServer-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServer-prod.modern.js @@ -2734,24 +2734,21 @@ function useFormState(action, initialState, permalink) { var formStateHookIndex = formStateCounter++, request = currentlyRenderingRequest; if ("function" === typeof action.$$FORM_ACTION) { - var keyJSON = JSON.stringify([ - currentlyRenderingKeyPath, - null, - formStateHookIndex - ]); + var nextPostbackStateKey = null, + componentKeyPath = currentlyRenderingKeyPath; request = request.formState; var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (null !== request && "function" === typeof isSignatureEqual) { - var postbackReferenceId = request[2], - postbackBoundArity = request[3]; - request[1] === keyJSON && - isSignatureEqual.call( - action, - postbackReferenceId, - postbackBoundArity - ) && - ((formStateMatchingIndex = formStateHookIndex), - (initialState = request[0])); + var postbackKey = request[1]; + isSignatureEqual.call(action, request[2], request[3]) && + ((nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + postbackKey === nextPostbackStateKey && + ((formStateMatchingIndex = formStateHookIndex), + (initialState = request[0]))); } var boundAction = action.bind(null, initialState); action = function (payload) { @@ -2760,9 +2757,17 @@ function useFormState(action, initialState, permalink) { "function" === typeof boundAction.$$FORM_ACTION && (action.$$FORM_ACTION = function (prefix) { prefix = boundAction.$$FORM_ACTION(prefix); + void 0 !== permalink && + ((permalink += ""), (prefix.action = permalink)); var formData = prefix.data; - formData && formData.append("$ACTION_KEY", keyJSON); - void 0 !== permalink && (prefix.action = permalink + ""); + formData && + (null === nextPostbackStateKey && + (nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + formData.append("$ACTION_KEY", nextPostbackStateKey)); return prefix; }); return [initialState, action]; @@ -4518,4 +4523,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-8898fe0f"; +exports.version = "18.3.0-www-modern-b31a402c"; diff --git a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js index d1c992c739eab..d2f3aee27e353 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-dev.modern.js @@ -9092,6 +9092,16 @@ function useOptimistic(passthrough, reducer) { return [passthrough, unsupportedSetOptimisticState]; } +function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) { + if (permalink !== undefined) { + return "p" + permalink; + } else { + // Append a node to the key path that represents the form state hook. + var keyPath = [componentKeyPath, null, hookIndex]; + return "k" + JSON.stringify(keyPath); + } +} + function useFormState(action, initialState, permalink) { resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to // track the position of this useFormState hook relative to the other ones in @@ -9105,32 +9115,43 @@ function useFormState(action, initialState, permalink) { if (typeof formAction === "function") { // This is a server action. These have additional features to enable // MPA-style form submissions with progressive enhancement. - // Determine the current form state. If we received state during an MPA form + // TODO: If the same permalink is passed to multiple useFormStates, and + // they all have the same action signature, Fizz will pass the postback + // state to all of them. We should probably only pass it to the first one, + // and/or warn. + // The key is lazily generated and deduped so the that the keypath doesn't + // get JSON.stringify-ed unnecessarily, and at most once. + var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form // submission, then we will reuse that, if the action identity matches. // Otherwise we'll use the initial state argument. We will emit a comment // marker into the stream that indicates whether the state was reused. - var state = initialState; // Append a node to the key path that represents the form state hook. - var componentKey = currentlyRenderingKeyPath; - var key = [componentKey, null, formStateHookIndex]; - var keyJSON = JSON.stringify(key); + var state = initialState; + var componentKeyPath = currentlyRenderingKeyPath; var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing] var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (postbackFormState !== null && typeof isSignatureEqual === "function") { - var postbackKeyJSON = postbackFormState[1]; + var postbackKey = postbackFormState[1]; var postbackReferenceId = postbackFormState[2]; var postbackBoundArity = postbackFormState[3]; if ( - postbackKeyJSON === keyJSON && isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity) ) { - // This was a match - formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); - state = postbackFormState[0]; + if (postbackKey === nextPostbackStateKey) { + // This was a match + formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form. + + state = postbackFormState[0]; + } } } // Bind the state to the first argument of the action. @@ -9143,19 +9164,29 @@ function useFormState(action, initialState, permalink) { if (typeof boundAction.$$FORM_ACTION === "function") { // $FlowIgnore[prop-missing] dispatch.$$FORM_ACTION = function (prefix) { - var metadata = boundAction.$$FORM_ACTION(prefix); - var formData = metadata.data; - - if (formData) { - formData.append("$ACTION_KEY", keyJSON); - } // Override the action URL + var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL if (permalink !== undefined) { { checkAttributeStringCoercion(permalink, "target"); } - metadata.action = permalink + ""; + permalink += ""; + metadata.action = permalink; + } + + var formData = metadata.data; + + if (formData) { + if (nextPostbackStateKey === null) { + nextPostbackStateKey = createPostbackFormStateKey( + permalink, + componentKeyPath, + formStateHookIndex + ); + } + + formData.append("$ACTION_KEY", nextPostbackStateKey); } return metadata; diff --git a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js index c5f18d5b53001..f25e38ed8a34b 100644 --- a/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js +++ b/compiled/facebook-www/ReactDOMServerStreaming-prod.modern.js @@ -2590,24 +2590,21 @@ function useFormState(action, initialState, permalink) { var formStateHookIndex = formStateCounter++, request = currentlyRenderingRequest; if ("function" === typeof action.$$FORM_ACTION) { - var keyJSON = JSON.stringify([ - currentlyRenderingKeyPath, - null, - formStateHookIndex - ]); + var nextPostbackStateKey = null, + componentKeyPath = currentlyRenderingKeyPath; request = request.formState; var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; if (null !== request && "function" === typeof isSignatureEqual) { - var postbackReferenceId = request[2], - postbackBoundArity = request[3]; - request[1] === keyJSON && - isSignatureEqual.call( - action, - postbackReferenceId, - postbackBoundArity - ) && - ((formStateMatchingIndex = formStateHookIndex), - (initialState = request[0])); + var postbackKey = request[1]; + isSignatureEqual.call(action, request[2], request[3]) && + ((nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + postbackKey === nextPostbackStateKey && + ((formStateMatchingIndex = formStateHookIndex), + (initialState = request[0]))); } var boundAction = action.bind(null, initialState); action = function (payload) { @@ -2616,9 +2613,17 @@ function useFormState(action, initialState, permalink) { "function" === typeof boundAction.$$FORM_ACTION && (action.$$FORM_ACTION = function (prefix) { prefix = boundAction.$$FORM_ACTION(prefix); + void 0 !== permalink && + ((permalink += ""), (prefix.action = permalink)); var formData = prefix.data; - formData && formData.append("$ACTION_KEY", keyJSON); - void 0 !== permalink && (prefix.action = permalink + ""); + formData && + (null === nextPostbackStateKey && + (nextPostbackStateKey = + void 0 !== permalink + ? "p" + permalink + : "k" + + JSON.stringify([componentKeyPath, null, formStateHookIndex])), + formData.append("$ACTION_KEY", nextPostbackStateKey)); return prefix; }); return [initialState, action]; diff --git a/compiled/facebook-www/ReactTestRenderer-dev.modern.js b/compiled/facebook-www/ReactTestRenderer-dev.modern.js index dc489424d54f3..b400b788384e9 100644 --- a/compiled/facebook-www/ReactTestRenderer-dev.modern.js +++ b/compiled/facebook-www/ReactTestRenderer-dev.modern.js @@ -24360,7 +24360,7 @@ function createFiberRoot( return root; } -var ReactVersion = "18.3.0-www-modern-0976aca4"; +var ReactVersion = "18.3.0-www-modern-f5e6bf9d"; // Might add PROFILE later.