Skip to content

Commit 88778ab

Browse files
committed
useFormState: Emit comment to mark whether state matches (#27307)
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: `<!--F-->` for a normal useFormState hook, and `<!--F!-->` 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 [8b26f07](8b26f07)
1 parent 089c36c commit 88778ab

13 files changed

+545
-162
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3566de59e2046e7e8478462375aaa71716f1095b
1+
8b26f07a883bb341c20283c0099bf5ee6f87bd1f

compiled/facebook-www/ReactDOM-dev.classic.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ var enableHostSingletons = true;
159159
var enableClientRenderFallbackOnTextMismatch = false;
160160

161161
var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation.
162+
var enableFormActions = false;
162163
var enableSuspenseCallback = true;
163164

164165
var FunctionComponent = 0;
@@ -34008,7 +34009,7 @@ function createFiberRoot(
3400834009
return root;
3400934010
}
3401034011

34011-
var ReactVersion = "18.3.0-www-classic-4045cb9c";
34012+
var ReactVersion = "18.3.0-www-classic-5b9b66e9";
3401234013

3401334014
function createPortal$1(
3401434015
children,
@@ -42938,7 +42939,8 @@ function getNextHydratable(node) {
4293842939
if (
4293942940
nodeData === SUSPENSE_START_DATA ||
4294042941
nodeData === SUSPENSE_FALLBACK_START_DATA ||
42941-
nodeData === SUSPENSE_PENDING_START_DATA
42942+
nodeData === SUSPENSE_PENDING_START_DATA ||
42943+
enableFormActions
4294242944
) {
4294342945
break;
4294442946
}

compiled/facebook-www/ReactDOM-dev.modern.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ var enableHostSingletons = true;
145145
var enableClientRenderFallbackOnTextMismatch = false;
146146

147147
var enableSchedulingProfiler = dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation.
148+
var enableFormActions = false;
148149
var enableSuspenseCallback = true;
149150

150151
var ReactSharedInternals =
@@ -33853,7 +33854,7 @@ function createFiberRoot(
3385333854
return root;
3385433855
}
3385533856

33856-
var ReactVersion = "18.3.0-www-modern-73585f43";
33857+
var ReactVersion = "18.3.0-www-modern-dd5c91c5";
3385733858

3385833859
function createPortal$1(
3385933860
children,
@@ -43448,7 +43449,8 @@ function getNextHydratable(node) {
4344843449
if (
4344943450
nodeData === SUSPENSE_START_DATA ||
4345043451
nodeData === SUSPENSE_FALLBACK_START_DATA ||
43451-
nodeData === SUSPENSE_PENDING_START_DATA
43452+
nodeData === SUSPENSE_PENDING_START_DATA ||
43453+
enableFormActions
4345243454
) {
4345343455
break;
4345443456
}

compiled/facebook-www/ReactDOMServer-dev.classic.js

Lines changed: 118 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-classic-605a9ce6";
22+
var ReactVersion = "18.3.0-www-classic-de28fc21";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -3153,6 +3153,15 @@ function pushStartOption(target, props, formatContext) {
31533153
return children;
31543154
}
31553155

3156+
var formStateMarkerIsMatching = stringToPrecomputedChunk("<!--F!-->");
3157+
var formStateMarkerIsNotMatching = stringToPrecomputedChunk("<!--F-->");
3158+
function pushFormStateMarkerIsMatching(target) {
3159+
target.push(formStateMarkerIsMatching);
3160+
}
3161+
function pushFormStateMarkerIsNotMatching(target) {
3162+
target.push(formStateMarkerIsNotMatching);
3163+
}
3164+
31563165
function pushStartForm(target, props, resumableState, renderState) {
31573166
target.push(startChunkForTag("form"));
31583167
var children = null;
@@ -9152,7 +9161,14 @@ var isReRender = false; // Whether an update was scheduled during the currently
91529161

91539162
var didScheduleRenderPhaseUpdate = false; // Counts the number of useId hooks in this component
91549163

9155-
var localIdCounter = 0; // Counts the number of use(thenable) calls in this component
9164+
var localIdCounter = 0; // Chunks that should be pushed to the stream once the component
9165+
// finishes rendering.
9166+
// Counts the number of useFormState calls in this component
9167+
9168+
var formStateCounter = 0; // The index of the useFormState hook that matches the one passed in at the
9169+
// root during an MPA navigation, if any.
9170+
9171+
var formStateMatchingIndex = -1; // Counts the number of use(thenable) calls in this component
91569172

91579173
var thenableIndexCounter = 0;
91589174
var thenableState = null; // Lazily created map of render-phase updates
@@ -9285,6 +9301,8 @@ function prepareToUseHooks(task, componentIdentity, prevThenableState) {
92859301
// workInProgressHook = null;
92869302

92879303
localIdCounter = 0;
9304+
formStateCounter = 0;
9305+
formStateMatchingIndex = -1;
92889306
thenableIndexCounter = 0;
92899307
thenableState = prevThenableState;
92909308
}
@@ -9298,6 +9316,8 @@ function finishHooks(Component, props, children, refOrContext) {
92989316
// restarting until no more updates are scheduled.
92999317
didScheduleRenderPhaseUpdate = false;
93009318
localIdCounter = 0;
9319+
formStateCounter = 0;
9320+
formStateMatchingIndex = -1;
93019321
thenableIndexCounter = 0;
93029322
numberOfReRenders += 1; // Start over from the beginning of the list
93039323

@@ -9319,6 +9339,18 @@ function checkDidRenderIdHook() {
93199339
// separate function to avoid using an array tuple.
93209340
var didRenderIdHook = localIdCounter !== 0;
93219341
return didRenderIdHook;
9342+
}
9343+
function getFormStateCount() {
9344+
// This should be called immediately after every finishHooks call.
9345+
// Conceptually, it's part of the return value of finishHooks; it's only a
9346+
// separate function to avoid using an array tuple.
9347+
return formStateCounter;
9348+
}
9349+
function getFormStateMatchingIndex() {
9350+
// This should be called immediately after every finishHooks call.
9351+
// Conceptually, it's part of the return value of finishHooks; it's only a
9352+
// separate function to avoid using an array tuple.
9353+
return formStateMatchingIndex;
93229354
} // Reset the internal hooks state if an error occurs while rendering a component
93239355

93249356
function resetHooksState() {
@@ -9608,7 +9640,11 @@ function useOptimistic(passthrough, reducer) {
96089640
}
96099641

96109642
function useFormState(action, initialState, permalink) {
9611-
resolveCurrentlyRenderingComponent(); // Bind the initial state to the first argument of the action.
9643+
resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component.
9644+
// TODO: We should also track which hook matches the form state passed at
9645+
// the root, if any. Matching is not yet implemented.
9646+
9647+
formStateCounter++; // Bind the initial state to the first argument of the action.
96129648
// TODO: Use the keypath (or permalink) to check if there's matching state
96139649
// from the previous page.
96149650

@@ -10400,6 +10436,8 @@ function renderIndeterminateComponent(
1040010436
legacyContext
1040110437
);
1040210438
var hasId = checkDidRenderIdHook();
10439+
var formStateCount = getFormStateCount();
10440+
var formStateMatchingIndex = getFormStateMatchingIndex();
1040310441

1040410442
{
1040510443
// Support for module components is deprecated and is removed behind a flag.
@@ -10432,30 +10470,79 @@ function renderIndeterminateComponent(
1043210470
{
1043310471
{
1043410472
validateFunctionComponentInDev(Component);
10435-
} // We're now successfully past this task, and we don't have to pop back to
10436-
// the previous task every again, so we can use the destructive recursive form.
10437-
10438-
if (hasId) {
10439-
// This component materialized an id. We treat this as its own level, with
10440-
// a single "child" slot.
10441-
var prevTreeContext = task.treeContext;
10442-
var totalChildren = 1;
10443-
var index = 0; // Modify the id context. Because we'll need to reset this if something
10444-
// suspends or errors, we'll use the non-destructive render path.
10445-
10446-
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
10447-
renderNode(request, task, value, 0); // Like the other contexts, this does not need to be in a finally block
10448-
// because renderNode takes care of unwinding the stack.
10449-
10450-
task.treeContext = prevTreeContext;
10451-
} else {
10452-
renderNodeDestructive(request, task, null, value, 0);
1045310473
}
10474+
10475+
finishFunctionComponent(
10476+
request,
10477+
task,
10478+
value,
10479+
hasId,
10480+
formStateCount,
10481+
formStateMatchingIndex
10482+
);
1045410483
}
1045510484

1045610485
popComponentStackInDEV(task);
1045710486
}
1045810487

10488+
function finishFunctionComponent(
10489+
request,
10490+
task,
10491+
children,
10492+
hasId,
10493+
formStateCount,
10494+
formStateMatchingIndex
10495+
) {
10496+
var didEmitFormStateMarkers = false;
10497+
10498+
if (formStateCount !== 0) {
10499+
// For each useFormState hook, emit a marker that indicates whether we
10500+
// rendered using the form state passed at the root.
10501+
// TODO: As an optimization, Fizz should only emit these markers if form
10502+
// state is passed at the root.
10503+
var segment = task.blockedSegment;
10504+
10505+
if (segment === null);
10506+
else {
10507+
didEmitFormStateMarkers = true;
10508+
var target = segment.chunks;
10509+
10510+
for (var i = 0; i < formStateCount; i++) {
10511+
if (i === formStateMatchingIndex) {
10512+
pushFormStateMarkerIsMatching(target);
10513+
} else {
10514+
pushFormStateMarkerIsNotMatching(target);
10515+
}
10516+
}
10517+
}
10518+
}
10519+
10520+
if (hasId) {
10521+
// This component materialized an id. We treat this as its own level, with
10522+
// a single "child" slot.
10523+
var prevTreeContext = task.treeContext;
10524+
var totalChildren = 1;
10525+
var index = 0; // Modify the id context. Because we'll need to reset this if something
10526+
// suspends or errors, we'll use the non-destructive render path.
10527+
10528+
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
10529+
renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block
10530+
// because renderNode takes care of unwinding the stack.
10531+
10532+
task.treeContext = prevTreeContext;
10533+
} else if (didEmitFormStateMarkers) {
10534+
// If there were formState hooks, we must use the non-destructive path
10535+
// because this component is not a pure indirection; we emitted markers
10536+
// to the stream.
10537+
renderNode(request, task, children, 0);
10538+
} else {
10539+
// We're now successfully past this task, and we haven't modified the
10540+
// context stack. We don't have to pop back to the previous task every
10541+
// again, so we can use the destructive recursive form.
10542+
renderNodeDestructive(request, task, null, children, 0);
10543+
}
10544+
}
10545+
1045910546
function validateFunctionComponentInDev(Component) {
1046010547
{
1046110548
if (Component) {
@@ -10541,22 +10628,16 @@ function renderForwardRef(request, task, prevThenableState, type, props, ref) {
1054110628
ref
1054210629
);
1054310630
var hasId = checkDidRenderIdHook();
10544-
10545-
if (hasId) {
10546-
// This component materialized an id. We treat this as its own level, with
10547-
// a single "child" slot.
10548-
var prevTreeContext = task.treeContext;
10549-
var totalChildren = 1;
10550-
var index = 0; // Modify the id context. Because we'll need to reset this if something
10551-
// suspends or errors, we'll use the non-destructive render path.
10552-
10553-
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
10554-
renderNode(request, task, children, 0); // Like the other contexts, this does not need to be in a finally block
10555-
// because renderNode takes care of unwinding the stack.
10556-
} else {
10557-
renderNodeDestructive(request, task, null, children, 0);
10558-
}
10559-
10631+
var formStateCount = getFormStateCount();
10632+
var formStateMatchingIndex = getFormStateMatchingIndex();
10633+
finishFunctionComponent(
10634+
request,
10635+
task,
10636+
children,
10637+
hasId,
10638+
formStateCount,
10639+
formStateMatchingIndex
10640+
);
1056010641
popComponentStackInDEV(task);
1056110642
}
1056210643

0 commit comments

Comments
 (0)