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

Explore to make JSX component abstract #6304

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Conversation

mununki
Copy link
Member

@mununki mununki commented Jun 20, 2023

discussion: #5725

This PR explores to make the JSX component as an abstract type. As a result of this PR change, the following type definition will cause a build error.

// build error
module M0 = {
  type props = string
  @module("A")
  external make: props => React.element = "default"
}

And now components that don't return React.element will throw a build error.

// build error
module M1 = {
  type props = {x: int}
  let make = (~x) => x
}

Note: With this change in PR, it looks like we're going to have to rewrite quite a bit of our tests. For now, I've commented out tests like /built_test/react_ppx for exploration.

@mununki mununki requested a review from cknitt June 20, 2023 17:52
@@ -7,11 +7,15 @@ module V4A = {
let make = (type a, ~a: a, ~b: array<option<[#Foo(a)]>>, ~c: 'a, _) => React.null
}

let _ = <V4A a="a" b=[] c=1 />
Copy link
Member Author

Choose a reason for hiding this comment

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

Withouth the component application, The type of this module contains type variables that cannot be generalized: A build error occurred.

Comment on lines +10 to +11
// @react.component
// let rec make = (~foo, ()) => React.createElement(make, makeProps(~foo, ()))
Copy link
Member Author

Choose a reason for hiding this comment

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

Should we also modify JSX3?

@cknitt
Copy link
Member

cknitt commented Jun 20, 2023

Awesome! Thanks! Will test tomorrow.

@mununki
Copy link
Member Author

mununki commented Jun 21, 2023

@cknitt Thanks! I'll keep going through the test cases and see if I can reduce the breaking changes.

@@ -36,7 +36,7 @@ external string: string => element = "%identity"
external array: array<element> => element = "%identity"

type componentLike<'props, 'return> = 'props => 'return
type component<'props> = componentLike<'props, element>
type component<'props>
Copy link
Member Author

Choose a reason for hiding this comment

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

This change will eventually affect JSX3 as well.

@cknitt
Copy link
Member

cknitt commented Jun 21, 2023

@mununki I tried to build one of our projects (fairly large, web + React Native) with this change.

And actually this didn't look too bad. All dependencies built successfully without any changes/patching. There were some issues in the project code itself though. I had little time today and haven't finished resolving all of them, but I have not come across a blocker yet. Will continue tomorrow.

One interesting thing was this:

@react.component
let make = (~x) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }

=>

  This expression's type contains type variables that cannot be generalized:
  React.component<props<_[> #a | #b]>>```

@mununki
Copy link
Member Author

mununki commented Jun 22, 2023

Yes, that is one of things to be fixed in this PR. The trasformation of component definition is changed in this PR as below:

// before
type props<'x> = {x: 'x}

let make = ({x, _}: props<_>) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }
let make = {
  let \"Abstract" = (props: props<_>) => make(props)

  \"Abstract"
}

// after
type props<'x> = {x: 'x}

let make = ({x, _}: props<_>) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }
let make = {
  let \"Abstract" = React.component((props: props<_>) => make(props)) // wrapping with React.component

  \"Abstract"
}

This change seems to interfere with the compiler's type inference. There are a couple other cases that I'm looking at.

@cknitt
Copy link
Member

cknitt commented Jun 22, 2023

The same thing occurs whenever there is a type variable in a prop type like in

@react.component
let make = (~x: 'x, ~render: 'x => React.element) => render(x)

I have something similar in one component, so that's blocking me now.

@cristianoc
Copy link
Collaborator

cristianoc commented Jun 22, 2023

Is there a way to approach this gradually?
I guess the treatment of component as functions is only problematic when FFI is used? (As there's no way to create a class component from ReScript, or is there).

@cknitt
Copy link
Member

cknitt commented Jun 22, 2023

Well, yes, but usually you will be using bindings to 3rd party libs when working with React (or writing bindings yourself), so you will be using FFI.

Even with just @rescript/react, e.g.

@react.component
let make = () => {
  React.Fragment.make({children: React.string("Boom")})
}

would compile fine and go 💥 at runtime. Maybe a contrived example, but still.

@cristianoc
Copy link
Collaborator

Asking in case one can special case the ffi only.

@cristianoc
Copy link
Collaborator

Yes, that is one of things to be fixed in this PR. The trasformation of component definition is changed in this PR as below:

// before
type props<'x> = {x: 'x}

let make = ({x, _}: props<_>) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }
let make = {
  let \"Abstract" = (props: props<_>) => make(props)

  \"Abstract"
}

// after
type props<'x> = {x: 'x}

let make = ({x, _}: props<_>) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }
let make = {
  let \"Abstract" = React.component((props: props<_>) => make(props)) // wrapping with React.component

  \"Abstract"
}

This change seems to interfere with the compiler's type inference. There are a couple other cases that I'm looking at.

You you can't do type inference under an application.
But, you could do this:

let make = {
  let \"Abstract" = (props: props<_>) => make(props)

   React.component(\"Abstract")
}

@mununki
Copy link
Member Author

mununki commented Sep 19, 2023

You you can't do type inference under an application. But, you could do this:

let make = {
  let \"Abstract" = (props: props<_>) => make(props)

   React.component(\"Abstract")
}

Wow! Thanks! I'll change and try it.

@mununki
Copy link
Member Author

mununki commented Sep 19, 2023

If I change the application location of the React.component as advised, I still get a build error. But if I add code that uses the component, the build succeeds regardless of the location of the React.component.

module C = {
  @react.component
  let make = (~x: 'x, ~render: 'x => React.element) => render(x)
}

// adding the component application, then build fine no matter `React.component` locations.
let _ = <C x=1 render={v => React.int(v)} />

@zth
Copy link
Collaborator

zth commented Oct 1, 2023

EDIT: Realize I posted in the wrong PR...

@mununki
Copy link
Member Author

mununki commented Oct 3, 2023

@cknitt Off the top of my head, if we were to make the type of the react component abstract, so that we could use the React.component identity function via ppx only to define the component type, we'd have one problem. We won't be able to define components like this without ppx.

module C = {
  type props = {a: string}
  let make = props => React.string(props.a)
}

So why don't we move towards making 'props => 'return' abstract only for component definitions that interop externally, as @cristianoc suggests?

@mununki
Copy link
Member Author

mununki commented May 24, 2024

Rebased to master

@mununki
Copy link
Member Author

mununki commented May 25, 2024

@mununki I tried to build one of our projects (fairly large, web + React Native) with this change.

And actually this didn't look too bad. All dependencies built successfully without any changes/patching. There were some issues in the project code itself though. I had little time today and haven't finished resolving all of them, but I have not come across a blocker yet. Will continue tomorrow.

One interesting thing was this:

@react.component
let make = (~x) =>
  switch x {
  | #a => React.string("A")
  | #b => React.string("B")
  | _ => React.string("other")
  }

=>

  This expression's type contains type variables that cannot be generalized:
  React.component<props<_[> #a | #b]>>```

I've discussed this issue with @cristianoc, we can summarize the issue to the example below:

type t<'a> = [> #a | #b] as 'a

let f = (x: t<_>) =>
  switch x {
  | #a => React.int(0)
  | #b => React.int(1)
  | _ => React.int(2)
  }

let component: ('props => 'element) => 'component = a => a

let \"H1" = component((x: t<_>) => f(x)) // error
let \"H2" = Obj.magic((x: t<_>) => f(x)) // Ok

The component approach seems not working with the type checker. The main purpose of making the jsx component as an abstract type is preventing the normal function signature from treating as a jsx component. Then, I'm thinking about a new strategy for the purpose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

4 participants