Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useFormState: Emit comment to mark whether state matches #27307

Merged
merged 1 commit into from
Sep 7, 2023

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Aug 29, 2023

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.

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Aug 29, 2023
@react-sizebot
Copy link

react-sizebot commented Aug 29, 2023

Comparing: 3566de5...18da28b

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js = 165.63 kB 165.63 kB = 51.88 kB 51.88 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.07% 174.70 kB 174.82 kB +0.14% 54.61 kB 54.69 kB
facebook-www/ReactDOM-prod.classic.js = 570.44 kB 570.44 kB = 100.45 kB 100.45 kB
facebook-www/ReactDOM-prod.modern.js = 554.21 kB 554.21 kB = 97.61 kB 97.61 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-server/cjs/react-server.development.js +1.32% 163.46 kB 165.62 kB +1.32% 40.26 kB 40.80 kB
oss-stable-semver/react-server/cjs/react-server.development.js +1.28% 152.48 kB 154.43 kB +1.25% 37.55 kB 38.02 kB
oss-stable/react-server/cjs/react-server.development.js +1.28% 152.48 kB 154.43 kB +1.25% 37.55 kB 38.02 kB
oss-stable-semver/react-server/cjs/react-server.production.min.js +0.94% 26.74 kB 26.99 kB +1.24% 8.92 kB 9.03 kB
oss-stable/react-server/cjs/react-server.production.min.js +0.94% 26.74 kB 26.99 kB +1.24% 8.92 kB 9.03 kB
oss-experimental/react-server/cjs/react-server.production.min.js +0.89% 28.82 kB 29.08 kB +1.08% 9.52 kB 9.63 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +0.69% 353.65 kB 356.08 kB +0.72% 78.26 kB 78.83 kB
facebook-www/ReactDOMServer-dev.modern.js +0.68% 358.46 kB 360.88 kB +0.72% 79.42 kB 79.99 kB
facebook-www/ReactDOMServer-dev.classic.js +0.66% 365.88 kB 368.31 kB +0.70% 81.07 kB 81.64 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +0.64% 363.19 kB 365.53 kB +0.69% 81.46 kB 82.02 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.64% 365.75 kB 368.08 kB +0.68% 81.94 kB 82.51 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.development.js +0.64% 383.17 kB 385.61 kB +0.69% 82.80 kB 83.37 kB
facebook-www/ReactDOMServer-prod.modern.js +0.64% 155.26 kB 156.25 kB +0.85% 28.15 kB 28.39 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +0.64% 367.66 kB 369.99 kB +0.69% 82.40 kB 82.96 kB
facebook-www/ReactDOMServer-prod.classic.js +0.63% 156.17 kB 157.15 kB +0.85% 28.39 kB 28.64 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.63% 371.79 kB 374.13 kB +0.69% 82.61 kB 83.19 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +0.63% 372.20 kB 374.54 kB +0.70% 82.74 kB 83.32 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +0.63% 157.41 kB 158.40 kB +0.79% 29.13 kB 29.36 kB
oss-experimental/react-dom/umd/react-dom-server.browser.development.js +0.63% 389.50 kB 391.95 kB +0.67% 83.53 kB 84.09 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.63% 373.09 kB 375.43 kB +0.70% 82.93 kB 83.51 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.development.js +0.61% 346.53 kB 348.66 kB +0.65% 77.85 kB 78.36 kB
oss-stable/react-dom/cjs/react-dom-server.bun.development.js +0.61% 346.56 kB 348.69 kB +0.65% 77.88 kB 78.39 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.61% 349.09 kB 351.22 kB +0.66% 78.33 kB 78.85 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.61% 349.12 kB 351.24 kB +0.65% 78.36 kB 78.87 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.development.js +0.61% 349.31 kB 351.44 kB +0.65% 78.78 kB 79.29 kB
oss-stable/react-dom/cjs/react-dom-server.browser.development.js +0.61% 349.34 kB 351.46 kB +0.66% 78.80 kB 79.32 kB
oss-stable-semver/react-dom/umd/react-dom-server-legacy.browser.development.js +0.61% 365.84 kB 368.06 kB +0.72% 79.14 kB 79.71 kB
oss-stable/react-dom/umd/react-dom-server-legacy.browser.development.js +0.61% 365.87 kB 368.09 kB +0.72% 79.17 kB 79.74 kB
oss-stable-semver/react-dom/umd/react-dom-server.browser.development.js +0.61% 366.06 kB 368.28 kB +0.72% 79.61 kB 80.19 kB
oss-stable/react-dom/umd/react-dom-server.browser.development.js +0.61% 366.09 kB 368.31 kB +0.72% 79.64 kB 80.22 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.development.js +0.61% 349.72 kB 351.85 kB +0.65% 78.90 kB 79.42 kB
oss-stable/react-dom/cjs/react-dom-server.edge.development.js +0.61% 349.75 kB 351.87 kB +0.65% 78.93 kB 79.44 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.development.js +0.61% 350.79 kB 352.91 kB +0.65% 78.82 kB 79.33 kB
oss-stable/react-dom/cjs/react-dom-server.node.development.js +0.61% 350.82 kB 352.94 kB +0.65% 78.85 kB 79.35 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.development.js +0.61% 351.00 kB 353.13 kB +0.65% 78.79 kB 79.31 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.development.js +0.61% 351.03 kB 353.15 kB +0.65% 78.82 kB 79.33 kB
oss-stable-semver/react-dom/cjs/react-dom-server.browser.production.min.js +0.36% 63.68 kB 63.91 kB +0.51% 19.71 kB 19.81 kB
oss-stable/react-dom/cjs/react-dom-server.browser.production.min.js +0.36% 63.71 kB 63.93 kB +0.51% 19.73 kB 19.83 kB
oss-stable-semver/react-dom/umd/react-dom-server.browser.production.min.js +0.35% 63.83 kB 64.05 kB +0.50% 20.00 kB 20.10 kB
oss-stable/react-dom/umd/react-dom-server.browser.production.min.js +0.35% 63.86 kB 64.08 kB +0.49% 20.02 kB 20.12 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +0.34% 68.96 kB 69.19 kB +0.46% 21.02 kB 21.11 kB
oss-stable-semver/react-dom/cjs/react-dom-server.edge.production.min.js +0.33% 67.91 kB 68.14 kB +0.46% 21.15 kB 21.24 kB
oss-stable/react-dom/cjs/react-dom-server.edge.production.min.js +0.33% 67.94 kB 68.17 kB +0.46% 21.17 kB 21.27 kB
oss-stable-semver/react-dom/cjs/react-dom-server.node.production.min.js +0.33% 67.97 kB 68.20 kB +0.43% 21.15 kB 21.24 kB
oss-stable/react-dom/cjs/react-dom-server.node.production.min.js +0.33% 68.00 kB 68.22 kB +0.43% 21.18 kB 21.27 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +0.33% 69.09 kB 69.32 kB +0.47% 21.30 kB 21.40 kB
oss-stable-semver/react-dom/umd/react-dom-server-legacy.browser.production.min.js +0.32% 62.89 kB 63.10 kB +0.43% 19.15 kB 19.23 kB
oss-stable/react-dom/umd/react-dom-server-legacy.browser.production.min.js +0.32% 62.92 kB 63.12 kB +0.42% 19.17 kB 19.26 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0.32% 62.73 kB 62.94 kB +0.41% 18.83 kB 18.90 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0.32% 62.76 kB 62.96 kB +0.41% 18.85 kB 18.93 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0.32% 66.11 kB 66.32 kB +0.36% 19.96 kB 20.03 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.production.min.js +0.32% 66.27 kB 66.47 kB +0.37% 20.35 kB 20.42 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.min.js +0.31% 73.41 kB 73.64 kB +0.42% 22.53 kB 22.62 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.min.js +0.31% 73.69 kB 73.92 kB +0.42% 22.71 kB 22.81 kB
oss-stable-semver/react-dom/cjs/react-dom-server.bun.production.min.js +0.31% 66.03 kB 66.24 kB +0.46% 20.13 kB 20.22 kB
oss-stable/react-dom/cjs/react-dom-server.bun.production.min.js +0.31% 66.06 kB 66.26 kB +0.45% 20.16 kB 20.25 kB
oss-stable-semver/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0.30% 67.61 kB 67.82 kB +0.36% 20.44 kB 20.52 kB
oss-stable/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0.30% 67.64 kB 67.84 kB +0.36% 20.47 kB 20.54 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.min.js +0.30% 69.66 kB 69.87 kB +0.43% 21.33 kB 21.42 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0.29% 71.24 kB 71.45 kB +0.36% 21.61 kB 21.69 kB

Generated by 🚫 dangerJS against 18da28b

Copy link
Collaborator

@gnoff gnoff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code makes sense but approach may be problematic with prerendering and hydration in general in the edge case of a form hook "above" the <html> of the page

Comment on lines +2016 to +2020
if (getIsHydrating()) {
// TODO: If this function returns true, it means we should use the form
// state passed to hydrateRoot instead of initialState.
tryToClaimNextHydratableFormMarkerInstance(currentlyRenderingFiber);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hydrating gate and the one in tryToClaim... are redundant. I assume there will be other logic in here so probably can drop the one in tryTo...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I did it that way to match the other similar functions in that file, which all have the guard at the top and also is redundant because the callers usually end up checking it regardless. Usually if I'm not the one who wrote a module I copy the style of the surrounding code for 1) consistency because I figure if I fix it here I should also fix it everywhere else 2) out of caution in case there was a good reason that I'm not thinking of :D

Comment on lines 1294 to 1317
export function canHydrateFormStateMarker(
instance: HydratableInstance,
): null | FormStateMarkerInstance {
const nodeData = (instance: any).data;
if (
nodeData === FORM_STATE_IS_MATCHING ||
nodeData === FORM_STATE_IS_NOT_MATCHING
) {
const markerInstance: FormStateMarkerInstance = (instance: any);
return markerInstance;
}
return null;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is this unfortunate forking of behavior in the canHydrate... functions where if we are in a Singleton like <body> or are in the container scope we assume there may be 3rd party nodes we need to skip over. If our nextHydratable was some injected 3rd party stylesheet then hydration would fail as this is currently implemented. I think this function needs. a similar treatment as canHydrateSuspenseInstance

This will make your optimization where we omit the marker when the hook is normal harder too b/c it'll need to be able to scan forward but also jump back to where it was if it doesn't find anything

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: the last point about the optimization, the idea is we wouldn't look for these nodes at all in that case. Basically it would revert to how it is today.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think this would still be a problem

function App() {
  ... = useFormState();
  return <div>...</div>
}


<script injectedBy3rdParty />
<!--!F-->
<div>
  ...
</div>

If I'm hydrating App the nextHydratableInstance will end up on <script injectedBy3rdParty /> Then when we check whether there is a form marker we see there isn't and we assume no postback state. Then we try to claim the next hydratable for <div>. We end up hitting the form state marker and stop causing a hydration mismatch.

Copy link
Collaborator Author

@acdlite acdlite Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that part makes sense, I just mean in the case where there's no postback state, there are no extra comment nodes at all, so all this code gets skipped anyway

Comment on lines 253 to 262
if (bufferedChunks !== null) {
// This component emitted some chunks. They are buffered until it finishes
// rendering successfully. We can flush them now.
const task: Task = (currentlyRenderingTask: any);
const target = task.blockedSegment.chunks;
// Not sure why Flow complains. If I change it to spread instead of apply,
// it checks fine.
// $FlowFixMe[method-unbinding]
target.push.apply(target, bufferedChunks);
}
Copy link
Collaborator

@gnoff gnoff Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is going to work in the edge case

function App() {
  let ... = useFormState();
  return (
    <html>
      <body>
        <form ...>
      </body>
    </html>
}

In Fizz (with float enabled) the html chunks are sequester so the fact that the comment is getting pushed to the target before the DOCTYPE does accidentally works out (we'll still emit <DOCTYPE html><html><head><!--F-->). However when hydrating on the client the starting point for hydration is document.body I believe (I can't remember if I made this change or I just talked about making it but conceptually we can start hydration from body b/c everything else is singleton which has it's own hydration path). If hydration starts from the body the form comment will be missed b/c it is in the head.

I don't have a good solution to this problem. We could maybe figure out if we need to put the comment before the <html> in the DOM by slotting it just after the DOCTYPE. Then we'd have to also special case the hydration cursor to look there. Additionally I think this may conflict with prerendering b/c we will have a static preamble (the html, head tag, and some head content) even when we can't fully prerender the entire shell. So we may have something dynamic (the form state indictator comment) that needs to be embedded inside the static part which just can't happen

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another crude way to do it could be to forbid useFormState outside of the body 😆

@@ -187,6 +193,8 @@ const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';
const FORM_STATE_IS_MATCHING = '!F';
Copy link
Collaborator

@sebmarkbage sebmarkbage Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I understand you're using this to mean NOT form state but the pattern that the other signaling comments use is:

TAG + STATE

So you can just check the first character to see if it matches the type of thing and then the second character is the state of that thing.

We should probably stick to that pattern. In theory if we did like switch(data[0]) it could matter.

@@ -187,6 +193,8 @@ const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';
const FORM_STATE_IS_MATCHING = '!F';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const FORM_STATE_IS_MATCHING = '!F';
const FORM_STATE_IS_MATCHING = 'F!';

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canHydrateFormStateMarker should skip past mismatches in root or host singletons but otherwise good.

@acdlite acdlite force-pushed the useformstate-hydration branch 2 times, most recently from feb5892 to d8844eb Compare September 7, 2023 19:56
A planned feature of useFormState is that if 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 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.
@acdlite acdlite merged commit 8b26f07 into facebook:main Sep 7, 2023
36 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 7, 2023
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)
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
)

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.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
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 commit 8b26f07.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants