-
Notifications
You must be signed in to change notification settings - Fork 455
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
Support async component for React Server component in JSX v4 #6399
Conversation
@zth Can you test with it? |
Looks good! Let me try it in a bit. We also need the external mentioned in the issue (#6398) and autowrap the function with that so what we get back is a valid React component, for this to be usable. |
Ah! You are correct. I missed that part. |
Tested and it compiles as expected 👍 |
There are two ways to do this
In a project using Server Component, do you see many patterns being used with client components of the React.componet type? If so, I think number 2 might be a little tricky. |
For example, it is not compiled let f = a => Js.Promise.resolve(a + a)
module C = {
@react.component
let make = async (~a) => {
let a = await f(a)
<div> {React.int(a)} </div>
}
}
let _ =
<div>
<C /> // Is this a common usage pattern?
</div>
|
I think way 1 is the best approach. React has stated that client components won't support async/await because of technical limitations, but they've changed things like this before. Conceptually, I'd say it's still a @cristianoc any thoughts? |
If I understand correctly. |
Yeah exactly. So it's the same situation as React users have in JS/TS. |
@mununki works great! Completion does not work for the component, but that's something for the editor tooling. |
I encounter the same issue to #6304 (comment) |
Is that one mostly about polyvariants/objects without type annotations? Or are there other cases too where it triggers/fails? |
Polymorphic type seems making failed. |
Any updates guys? |
Me and @cristianoc had a chat about this and identity functions won't work for wrapping the full function. But, here's an approach that does seem to work: external reactPromise: promise<React.element> => React.element = "%identity"
module AsyncComponent = {
type props<'status> = {status: 'status}
let make = async ({status}) => {
switch status {
| #on => React.string("on")
| #off => React.string("off")
}
}
let make = props => {
make(props)->reactPromise
}
}
let jsx = <AsyncComponent status=#on /> @mununki what do you think? |
It seems working! let f = a => Js.Promise.resolve(a + a)
module C = {
@react.component
let make = async (~a) => {
let a = await f(a)
<div> {React.int(a)} </div>
}
}
let _ =
<div>
<C a=1 />
</div>
module C1 = {
@react.component
let make = async (~status) => {
switch status {
| #on => React.string("on")
| #off => React.string("off")
}
}
}
let jsx = <C1 status=#on /> |
Fantastic! 😍 What do you think about this approach, good to you? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All seems to be working, right? @cristianoc could have a look as well perhaps
Thanks to your hint, I realized that the PRs should be approached differently - the two PRs had similar approaches, but different problems to solve. Thank you. |
The other PR I mentioned is #6304 |
Js output looks fine to me. Curious about how it runs in the runtime of RSC. |
@mununki it seems to work well, although I'm slightly confused by the JS output. Here's my example in code ( type user = {
id: string,
name: string,
}
let getUser = async id => {
{id, name: id}
}
@react.component
let make = async (~id) => {
let user = await getUser(id)
<div> {React.string(user.name)} </div>
} And here's the emitted JS: // Generated by ReScript, PLEASE EDIT WITH CARE
import * as JsxRuntime from "react/jsx-runtime";
async function getUser(id) {
return {
id: id,
name: id
};
}
async function make(param) {
var user = await getUser(param.id);
return JsxRuntime.jsx("div", {
children: user.name
});
}
var ServerComponent = make;
var make$1 = ServerComponent;
export {
getUser ,
make$1 as make,
}
/* react/jsx-runtime Not a pure module */ This is great and works as intended, but I don't see the intermediate function that casts to React.element emitted anywhere. Is that perhaps optimized away? That's great if so. EDIT: To be clear, I've tested the above in a Next JS app using server components. And it works as expected. |
When I looked at the rawlambda, it appears to be optimized and gone, as you said. module React = {
let string = s => s
}
type user = {
id: string,
name: string,
}
let getUser = async id => {
{id, name: id}
}
@react.component
let make = async (~id) => {
let user = await getUser(id)
{React.string(user.name)}
}
|
That's perfect, better than I expected. Good technique to keep in mind for future use cases. One last look from @cristianoc and then we should be good to go, right. |
@@ -63,3 +63,28 @@ let removeArity binding = | |||
| _ -> expr | |||
in | |||
{binding with pvb_expr = removeArityRecord binding.pvb_expr} | |||
|
|||
let add_async_attribute ~async (body : Parsetree.expression) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these functions already in the file that deals with async transformations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean the files that deal with async transformation are ast_async.ml
or ast_attributes.ml
, the syntax
module doesn't have the frontend
as dependency yet. That's why I added here again.
Do you want me to add the frontend module to deps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, not able to do that due do circular deps: frontend -> common -> syntax
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like front-end already accesses ml
:
(library
(name frontend)
(wrapped false)
(flags
(:standard -w +a-4-9-40-42-70))
(libraries common ml))
and ml
is where e.g. ast_untagged_variants.ml
is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something. syntax
can access ml
, but not frontend
that has async transformation functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps my wording is confusing. Actually, this is correct: frontend <- common <- syntax
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put it where ast_untagged_variants.ml
is, and change nothing else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not allowed to use anything that is not already in ml
, obviously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great.
Left some comments about potential code duplication.
@@ -63,3 +63,18 @@ let removeArity binding = | |||
| _ -> expr | |||
in | |||
{binding with pvb_expr = removeArityRecord binding.pvb_expr} | |||
|
|||
let is_async : Parsetree.attribute -> bool = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would you move this function too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does it look? 9d283ba
I moved is_async
and is_await
to ast_async
and ast_await
instead of moving ast_attributes.ml
to ml
. If we are going to move ast_attributes.ml
, then other modules need to be moved too such as external_arg_spec.ml
, bs_syntaxerr.ml
. I think that is a bit too much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great.
Good to merge
let is_async : Parsetree.attribute -> bool = | ||
fun ({txt}, _) -> txt = "async" || txt = "res.async" | ||
|
||
let async_component ~async expr = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should not move as it's specific to the ppx
Resolve #6398
I've made it so that where the user defines async is considered an attribute of pvb_expr = Pexp_fun of component function. If you want the user to use it elsewhere, this will require additional implementation, let me know if you have any ideas.