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: Reuse state from previous form submission #27321

Merged
merged 2 commits into from
Sep 13, 2023

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Aug 31, 2023

If a Server Action is passed to useFormState, the action may be submitted before it has hydrated. This will trigger a full page (MPA-style) navigation. We can transfer the form state to the next page by comparing the key path of the hook instance.

ReactServerDOMServer.decodeFormState is used by the server to extract the form state from the submitted action. This value can then be passed as an option when rendering the new page. It must be passed during both SSR and hydration.

const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });

If the formState option is omitted, then the state won't be transferred to the next page. However, it must be passed in both places, or in neither; misconfiguring will result in a hydration mismatch.

(The formState option is currently prefixed with experimental_)

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Aug 31, 2023
@acdlite acdlite marked this pull request as draft August 31, 2023 17:34
@react-sizebot
Copy link

react-sizebot commented Aug 31, 2023

Comparing: e520565...765ceb2

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 +0.14% 166.38 kB 166.62 kB +0.10% 52.08 kB 52.13 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.24% 175.64 kB 176.05 kB +0.15% 54.88 kB 54.97 kB
facebook-www/ReactDOM-prod.classic.js +0.02% 571.59 kB 571.73 kB +0.02% 100.62 kB 100.64 kB
facebook-www/ReactDOM-prod.modern.js +0.02% 555.37 kB 555.46 kB +0.02% 97.74 kB 97.75 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.04% 166.94 kB 168.68 kB +1.07% 40.99 kB 41.43 kB
oss-experimental/react-server/cjs/react-server.production.min.js +0.92% 29.43 kB 29.70 kB +1.32% 9.72 kB 9.85 kB
facebook-www/ReactDOMServer-prod.modern.js +0.54% 156.20 kB 157.04 kB +0.69% 28.42 kB 28.62 kB
facebook-www/ReactDOMServer-prod.classic.js +0.54% 156.87 kB 157.71 kB +0.68% 28.63 kB 28.83 kB
facebook-www/ReactDOMServerStreaming-prod.modern.js +0.50% 158.35 kB 159.14 kB +0.67% 29.37 kB 29.57 kB
facebook-www/ReactDOMServerStreaming-dev.modern.js +0.49% 348.49 kB 350.21 kB +0.53% 77.61 kB 78.02 kB
facebook-www/ReactDOMServer-dev.modern.js +0.49% 353.30 kB 355.02 kB +0.52% 78.75 kB 79.16 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.development.js +0.48% 358.15 kB 359.88 kB +0.52% 80.76 kB 81.18 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.development.js +0.48% 366.76 kB 368.51 kB +0.53% 81.93 kB 82.37 kB
facebook-www/ReactDOMServer-dev.classic.js +0.48% 360.73 kB 362.45 kB +0.51% 80.40 kB 80.81 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.development.js +0.48% 367.17 kB 368.92 kB +0.53% 82.05 kB 82.49 kB
oss-experimental/react-dom/cjs/react-dom-server.node.development.js +0.48% 368.06 kB 369.81 kB +0.53% 82.25 kB 82.68 kB
oss-experimental/react-dom/umd/react-dom-server.browser.development.js +0.47% 384.38 kB 386.21 kB +0.51% 82.81 kB 83.23 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.47% 97.24 kB 97.70 kB +0.69% 23.18 kB 23.34 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.47% 97.24 kB 97.70 kB +0.69% 23.18 kB 23.34 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.development.js +0.46% 360.71 kB 362.38 kB +0.50% 81.21 kB 81.61 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.development.js +0.46% 378.04 kB 379.79 kB +0.50% 82.08 kB 82.49 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.development.js +0.46% 362.62 kB 364.29 kB +0.49% 81.66 kB 82.07 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.46% 100.96 kB 101.42 kB +0.65% 24.38 kB 24.54 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.46% 100.96 kB 101.42 kB +0.65% 24.38 kB 24.54 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.45% 101.37 kB 101.83 kB +0.65% 24.49 kB 24.65 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.45% 101.37 kB 101.83 kB +0.65% 24.49 kB 24.65 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.development.js +0.45% 101.65 kB 102.11 kB +0.67% 23.94 kB 24.10 kB
oss-stable-semver/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +0.45% 106.76 kB 107.24 kB +0.64% 24.75 kB 24.91 kB
oss-stable/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +0.45% 106.76 kB 107.24 kB +0.64% 24.75 kB 24.91 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.44% 104.67 kB 105.13 kB +0.65% 24.97 kB 25.14 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.44% 104.67 kB 105.13 kB +0.65% 24.97 kB 25.14 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js +0.44% 105.82 kB 106.28 kB +0.63% 25.27 kB 25.43 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js +0.43% 106.23 kB 106.69 kB +0.61% 25.40 kB 25.55 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.43% 106.71 kB 107.17 kB +0.62% 25.58 kB 25.74 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.43% 106.71 kB 107.17 kB +0.62% 25.58 kB 25.74 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.development.js +0.43% 111.86 kB 112.34 kB +0.62% 25.63 kB 25.79 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.development.js +0.42% 109.08 kB 109.54 kB +0.63% 25.73 kB 25.89 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.development.js +0.41% 111.13 kB 111.59 kB +0.63% 26.31 kB 26.48 kB
oss-stable-semver/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +0.40% 24.69 kB 24.79 kB +0.38% 8.60 kB 8.63 kB
oss-stable/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +0.40% 24.69 kB 24.79 kB +0.38% 8.60 kB 8.63 kB
oss-experimental/react-server-dom-esm/cjs/react-server-dom-esm-server.node.production.min.js +0.38% 25.85 kB 25.95 kB +0.37% 8.88 kB 8.92 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +0.38% 26.07 kB 26.17 kB +0.34% 8.89 kB 8.92 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +0.38% 26.07 kB 26.17 kB +0.34% 8.89 kB 8.92 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +0.38% 26.39 kB 26.49 kB +0.32% 9.01 kB 9.03 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +0.38% 26.39 kB 26.49 kB +0.32% 9.01 kB 9.03 kB
oss-experimental/react-dom/cjs/react-dom-server.browser.production.min.js +0.37% 68.76 kB 69.01 kB +0.66% 21.12 kB 21.26 kB
oss-experimental/react-dom/umd/react-dom-server.browser.production.min.js +0.37% 68.89 kB 69.14 kB +0.64% 21.38 kB 21.51 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.production.min.js +0.36% 27.28 kB 27.38 kB +0.33% 9.20 kB 9.23 kB
oss-stable-semver/react-server/cjs/react-server.production.min.js +0.36% 27.31 kB 27.41 kB +0.31% 9.14 kB 9.17 kB
oss-stable/react-server/cjs/react-server.production.min.js +0.36% 27.31 kB 27.41 kB +0.31% 9.14 kB 9.17 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +0.36% 27.33 kB 27.43 kB +0.35% 9.33 kB 9.37 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +0.36% 27.33 kB 27.43 kB +0.35% 9.33 kB 9.37 kB
oss-stable-semver/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +0.36% 26.24 kB 26.34 kB +0.38% 9.01 kB 9.04 kB
oss-stable/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +0.36% 26.24 kB 26.34 kB +0.38% 9.01 kB 9.04 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.production.min.js +0.36% 27.60 kB 27.70 kB +0.32% 9.30 kB 9.33 kB
oss-stable-semver/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +0.36% 27.86 kB 27.96 kB +0.35% 9.49 kB 9.52 kB
oss-stable/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +0.36% 27.86 kB 27.96 kB +0.35% 9.49 kB 9.52 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.unbundled.production.min.js +0.35% 28.49 kB 28.59 kB +0.34% 9.63 kB 9.66 kB
oss-experimental/react-dom/cjs/react-dom-server.edge.production.min.js +0.35% 73.21 kB 73.46 kB +0.55% 22.63 kB 22.75 kB
oss-experimental/react-server-dom-webpack/umd/react-server-dom-webpack-server.browser.production.min.js +0.35% 27.46 kB 27.55 kB +0.33% 9.32 kB 9.35 kB
oss-experimental/react-dom/cjs/react-dom-server.node.production.min.js +0.35% 73.48 kB 73.74 kB +0.53% 22.80 kB 22.92 kB
oss-experimental/react-dom/cjs/react-dom-server.bun.production.min.js +0.34% 69.44 kB 69.67 kB +0.48% 21.42 kB 21.52 kB
oss-experimental/react-server-dom-webpack/cjs/react-server-dom-webpack-server.node.production.min.js +0.34% 29.02 kB 29.11 kB +0.34% 9.79 kB 9.82 kB
oss-experimental/react-dom/umd/react-dom-server-legacy.browser.production.min.js +0.31% 66.04 kB 66.25 kB +0.62% 20.40 kB 20.53 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.browser.production.min.js +0.31% 65.89 kB 66.09 kB +0.55% 20.02 kB 20.13 kB
oss-experimental/react-dom/cjs/react-dom-server-legacy.node.production.min.js +0.29% 71.02 kB 71.22 kB +0.52% 21.69 kB 21.80 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.24% 175.64 kB 176.05 kB +0.15% 54.88 kB 54.97 kB
oss-experimental/react-dom/umd/react-dom.production.min.js +0.23% 175.41 kB 175.82 kB +0.20% 55.13 kB 55.24 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.min.js +0.23% 181.85 kB 182.26 kB +0.16% 57.21 kB 57.30 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +0.22% 185.28 kB 185.70 kB +0.18% 57.26 kB 57.37 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +0.22% 184.42 kB 184.82 kB +0.15% 57.50 kB 57.58 kB

Generated by 🚫 dangerJS against 765ceb2

@acdlite acdlite force-pushed the useformstate-mpa branch 3 times, most recently from 6df391d to 79ca08f Compare August 31, 2023 18:38
@acdlite acdlite force-pushed the useformstate-mpa branch 6 times, most recently from a0e218e to 01c7d6c Compare September 11, 2023 22:43
acdlite added a commit that referenced this pull request Sep 13, 2023
…7366)

Currently, if a component suspends, the keyPath has already been
modified to include the identity of the component itself; the path is
set before the component body is called (akin to the begin phase in
Fiber). An accidental consequence is that when the promise resolves and
component is retried, the identity gets appended to the keyPath again,
leading to a duplicate node in the path.

To address this, we should only modify contexts after any code that may
suspend. For maximum safety, this should occur as late as possible:
right before the recursive renderNode call, before the children are
rendered.

I did not add a test yet because there's no feature that currently
observes it, but I do have tests in my other WIP PR for useFormState:
#27321
github-actions bot pushed a commit that referenced this pull request Sep 13, 2023
…7366)

Currently, if a component suspends, the keyPath has already been
modified to include the identity of the component itself; the path is
set before the component body is called (akin to the begin phase in
Fiber). An accidental consequence is that when the promise resolves and
component is retried, the identity gets appended to the keyPath again,
leading to a duplicate node in the path.

To address this, we should only modify contexts after any code that may
suspend. For maximum safety, this should occur as late as possible:
right before the recursive renderNode call, before the children are
rendered.

I did not add a test yet because there's no feature that currently
observes it, but I do have tests in my other WIP PR for useFormState:
#27321

DiffTrain build for [d07921e](d07921e)
@acdlite acdlite force-pushed the useformstate-mpa branch 4 times, most recently from 66035c5 to b127cfb Compare September 13, 2023 02:47
@acdlite acdlite marked this pull request as ready for review September 13, 2023 02:47
);

ReactDOM.hydrateRoot(document, <Shell data={root} />, {
experimental_formState: formState,
Copy link
Collaborator

Choose a reason for hiding this comment

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

So since we're serializing formState (nor the rest of the RSC payload) submitting this without JS and then hydrating the result will result in a hydration mismatch in the fixture, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, yes. Didn't catch that because I disabled hydration entirely when I was testing. Should add a button or something to start hydration asynchronously.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not actually sure how to transport this since it's a separate process. Seems like it'd have to be encoded into the HTML.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea, Next.js encodes the stream as text into the html (which doesn't allow for binary). We didn't bother doing that here because we wanted a more built-in mechanism for this upstream anyway. Seems fine to leave with a comment for now.


// This is an opaque type returned by decodeFormState on the server, but it's
// defined in this shared file because the same type is used by React on
// the client.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems like we should just pass the string value through. This shouldn't really be shared with the client since it doesn't know the format and the format could change to just a string for example. It might not even be JSON. I think of the key as just a string similar to a key on a React Element.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It also indicates that there's not parity since Flight does JSON.parse and Fizz does JSON.stringify. So there's not one owner. Seems like Fizz should be the sole owner and do the JSON.parse part and while passing around it's just a string. This means that in the RSC protocol it'll be encoded twice as a JSON string inside of JSON but really it's just an id. Could be a hash too.

Copy link
Collaborator

@sebmarkbage sebmarkbage Sep 13, 2023

Choose a reason for hiding this comment

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

I guess ReactFormState<S> would still be an opaque type here but KeyNode can go away and live in Fizz.

@@ -577,22 +587,76 @@ function useOptimistic<S, A>(
return [passthrough, unsupportedSetOptimisticState];
}

function isSameFormStateInstance(k1: KeyNode, k2: KeyNode): boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we accepted a string as the argument to Fizz instead, this could just be JSON.stringify(key) === formState.key. That would also allow us to optionally apply a hash to the key.

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.

Let's make the key a string so we can hide the data structure and make it hashable.

If a Server Action is passed to useFormState, the action may be submitted
before it has hydrated. This will trigger a full page (MPA-style) navigation.
We can transfer the form state to the next page by comparing the key path
of the hook instance.

`ReactServerDOMServer.decodeFormState` is used by the server to extract the form
state from the submitted action. This value can then be passed as an option
when rendering the new page. It must be passed during both SSR and hydration.

```js
const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });
```

If the `formState` option is omitted, then the state won't be transferred to
the next page. However, it must be passed in both places, or in neither;
misconfiguring will result in a hydration mismatch.
Updates the Flight fixture's Counter component to useFormState instead
of useState.
@acdlite acdlite merged commit 612b2b6 into facebook:main Sep 13, 2023
36 checks passed
github-actions bot pushed a commit that referenced this pull request Sep 13, 2023
If a Server Action is passed to useFormState, the action may be
submitted before it has hydrated. This will trigger a full page
(MPA-style) navigation. We can transfer the form state to the next page
by comparing the key path of the hook instance.

`ReactServerDOMServer.decodeFormState` is used by the server to extract
the form state from the submitted action. This value can then be passed
as an option when rendering the new page. It must be passed during both
SSR and hydration.

```js
const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });
```

If the `formState` option is omitted, then the state won't be
transferred to the next page. However, it must be passed in both places,
or in neither; misconfiguring will result in a hydration mismatch.

(The `formState` option is currently prefixed with `experimental_`)

DiffTrain build for [612b2b6](612b2b6)
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
…cebook#27366)

Currently, if a component suspends, the keyPath has already been
modified to include the identity of the component itself; the path is
set before the component body is called (akin to the begin phase in
Fiber). An accidental consequence is that when the promise resolves and
component is retried, the identity gets appended to the keyPath again,
leading to a duplicate node in the path.

To address this, we should only modify contexts after any code that may
suspend. For maximum safety, this should occur as late as possible:
right before the recursive renderNode call, before the children are
rendered.

I did not add a test yet because there's no feature that currently
observes it, but I do have tests in my other WIP PR for useFormState:
facebook#27321
EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
If a Server Action is passed to useFormState, the action may be
submitted before it has hydrated. This will trigger a full page
(MPA-style) navigation. We can transfer the form state to the next page
by comparing the key path of the hook instance.

`ReactServerDOMServer.decodeFormState` is used by the server to extract
the form state from the submitted action. This value can then be passed
as an option when rendering the new page. It must be passed during both
SSR and hydration.

```js
const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });
```

If the `formState` option is omitted, then the state won't be
transferred to the next page. However, it must be passed in both places,
or in neither; misconfiguring will result in a hydration mismatch.

(The `formState` option is currently prefixed with `experimental_`)
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
…7366)

Currently, if a component suspends, the keyPath has already been
modified to include the identity of the component itself; the path is
set before the component body is called (akin to the begin phase in
Fiber). An accidental consequence is that when the promise resolves and
component is retried, the identity gets appended to the keyPath again,
leading to a duplicate node in the path.

To address this, we should only modify contexts after any code that may
suspend. For maximum safety, this should occur as late as possible:
right before the recursive renderNode call, before the children are
rendered.

I did not add a test yet because there's no feature that currently
observes it, but I do have tests in my other WIP PR for useFormState:
#27321

DiffTrain build for commit d07921e.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
If a Server Action is passed to useFormState, the action may be
submitted before it has hydrated. This will trigger a full page
(MPA-style) navigation. We can transfer the form state to the next page
by comparing the key path of the hook instance.

`ReactServerDOMServer.decodeFormState` is used by the server to extract
the form state from the submitted action. This value can then be passed
as an option when rendering the new page. It must be passed during both
SSR and hydration.

```js
const boundAction = await decodeAction(formData, serverManifest);
const result = await boundAction();
const formState = decodeFormState(result, formData, serverManifest);

// SSR
const response = createFromReadableStream(<App />);
const ssrStream = await renderToReadableStream(response, { formState })

// Hydration
hydrateRoot(container, <App />, { formState });
```

If the `formState` option is omitted, then the state won't be
transferred to the next page. However, it must be passed in both places,
or in neither; misconfiguring will result in a hydration mismatch.

(The `formState` option is currently prefixed with `experimental_`)

DiffTrain build for commit 612b2b6.
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.

4 participants