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] Allow sync actions #27571

Merged
merged 3 commits into from
Nov 1, 2023

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Oct 24, 2023

Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the server. But since we support client-side actions, too, there's no reason we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler but it's not really that much more complicated because we already support this for actions passed to startTransition. So now it's consistent: anywhere an action is accepted, a sync client function is a valid input.

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

react-sizebot commented Oct 24, 2023

Comparing: 960ed6e...e08ebb1

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.09% 175.04 kB 175.20 kB +0.10% 54.46 kB 54.52 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +0.09% 177.17 kB 177.32 kB +0.05% 55.15 kB 55.18 kB
facebook-www/ReactDOM-prod.classic.js = 567.61 kB 567.61 kB = 99.94 kB 99.94 kB
facebook-www/ReactDOM-prod.modern.js = 551.47 kB 551.47 kB = 97.04 kB 97.04 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against e08ebb1

@@ -76,7 +76,7 @@ export function useFormStatus(): FormStatus {
}

export function useFormState<S, P>(
action: (S, P) => Promise<S>,
action: (S, P) => S | Promise<S>,
Copy link
Collaborator

@sebmarkbage sebmarkbage Oct 24, 2023

Choose a reason for hiding this comment

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

This is not quite right. In TypeScript I think this would be:

export function useFormState<S, P>(
  action: (Awaited<S>, P) => S,
  initialState: Awaited<S>,
  permalink?: string,
): [Awaited<S>, (P) => void] {

So if S is a Promise, the state is the awaited result. If S is not a promise it is the same as S.

Is there some equivalent in Flow?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

there’s not one built in but I asked the Flow team about it and they showed me how to create an equivalent, just forgot to update the PR

Copy link
Contributor

@unstubbable unstubbable Oct 24, 2023

Choose a reason for hiding this comment

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

I can't speak for Flow, but in TypeScript the action type as chosen by Andrew is the idiomatic way of declaring a callback that can be sync or async, if I'm not mistaken. The usage of Awaited as proposed here would actually lead to a compiler error for async actions, as demonstrated in this TypeScript playground.

Copy link
Contributor

@unstubbable unstubbable Oct 24, 2023

Choose a reason for hiding this comment

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

If your intention with using Awaited is to disallow a promise being used as initial state, which the type did not prevent before but maybe should, a possible solution would be to use a combination of both proposed signatures:

function useFormState<S, P>(
  action: (prevState: Awaited<S>, payload: P) => S | Promise<S>,
  initialState: Awaited<S>,
  permalink?: string,
): [Awaited<S>, (payload: P) => void];

See also this playground with a comparison of all three solutions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Huh, I don't really understand why the S | Promise<S> thing is necessary. I'm just going to let @eps1lon figure this out in the DefinitelyTyped repo since that's the one that actually matters 😆 For our internal Flow types, I updated it to @sebmarkbage 's version (165424e), which mostly worked but I did hit a few "recursion limit exceeded" errors and ended up needing to resort to any in a few places anyway. But as long as the public ones are correct it's not that important.

@acdlite acdlite force-pushed the useformstate-allow-sync-action branch 3 times, most recently from 5bfbd56 to 2202499 Compare October 24, 2023 23:27
@@ -184,3 +184,13 @@ export type ReactFormState<S, ReferenceId> = [
ReferenceId /* Server Reference ID */,
number /* number of bound arguments */,
];

export type Awaited<T> = T extends null | void
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copied this from the TypeScript codebase

Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is
a valid input.
@acdlite acdlite force-pushed the useformstate-allow-sync-action branch 3 times, most recently from d6a6d45 to 9a604dc Compare October 25, 2023 23:58
Needed to bump this to get support for Flow's `extends` syntax.
Updates our internal Flow type for useFormState to allow for synchronous
actions. I tried to make it match what the TypeScript equivalent would
be, so although Flow does not include an Awaited type helper, I copied
the one from TypeScript and added it to our internal types.

Unfortunately this led to a few "Recursion limit exceeded" Flow errors
in the Fiber implementation. I wasn't able to figure out whether these
were legit type errors, or if the types were correct but the
recursiveness of the types tripped some internal limit. But since this
only affects the type coverage of our internal implementation and not
the public API, I sprinkled in some `any`s and called it a day.
@acdlite acdlite force-pushed the useformstate-allow-sync-action branch from 9a604dc to e08ebb1 Compare October 26, 2023 00:19
@acdlite acdlite merged commit 77c4ac2 into facebook:main Nov 1, 2023
36 checks passed
github-actions bot pushed a commit that referenced this pull request Nov 1, 2023
Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.

DiffTrain build for [77c4ac2](77c4ac2)
permalink?: string,
) => [S, (P) => void],
) => [Awaited<S>, (P) => void],
Copy link
Collaborator

Choose a reason for hiding this comment

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

As far as I can tell, we only need the Awaited in action and not initialState nor return value: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/67266/files

Let me know if I missed some scenarios that should be rejected at the type-level

Copy link
Contributor

@unstubbable unstubbable Nov 1, 2023

Choose a reason for hiding this comment

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

When I tested this last week, passing in a promise as initial state, I got this (rather misleading) error:

async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.

But the types also did not prevent this before DefinitelyTyped/DefinitelyTyped#67266.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah never mind, it does give you a type error in your variant, so it's all fine I guess.

EdisonVan pushed a commit to EdisonVan/react that referenced this pull request Apr 15, 2024
Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.
bigfootjon pushed a commit that referenced this pull request Apr 18, 2024
Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.

DiffTrain build for commit 77c4ac2.
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.

6 participants