[Call for help #2] Test JSX v4

Why are we requiring React 18.2 in @rescript/react / why is 18.1 or 18.0 not sufficient? (The reason I am asking is that React Native is currently still on 18.1.)

I think 18.0 would be fine.

We can set 18.x in a package json

1 Like

Just realized that the context API can now be used as follows:

let context = React.createContext("example")

module Provider = {
  type props = React.Context.props<string>
  let make = React.Context.provider(context)
}

<Provider value="something">
  <SomeComponent />
</Provider>
6 Likes

I have discovered a problem in representing APIs that take a React component with a certain props shape as an argument. (Or, to be more precise, with using @react.component for the components to pass to such an API.) For example, React Navigation screens.

So, in JSX v3, something like this works:

module SomeLib = {
  type navigation
  type route

  type screenProps = {"navigation": navigation, "route": route}

  type screenComponent = React.component<screenProps>

  module Screen = {
    @react.component @module("some-lib")
    external make: (~component: screenComponent) => React.element = "Screen"
  }
}

module SomeComponent = {
  @react.component
  let make = (~navigation as _: SomeLib.navigation, ~route as _: SomeLib.route) =>
    React.string("Some component")
}

let render = () => <SomeLib.Screen component=SomeComponent.make />

but in JSX v4, changing screenProps to a record type, this does not work anymore because of nominal typing:

module SomeLib = {
  type navigation
  type route

  type screenProps = {navigation: navigation, route: route}

  type screenComponent = React.component<screenProps>

  module Screen = {
    @react.component @module("some-lib")
    external make: (~component: screenComponent) => React.element = "Screen"
  }
}

module SomeComponent = {
  @react.component
  let make = (~navigation as _: SomeLib.navigation, ~route as _: SomeLib.route) =>
    React.string("Some component")
}

let render = () => <SomeLib.Screen component=SomeComponent.make />

It gives the error:

This has type:
  SomeComponent.props<SomeLib.navigation, SomeLib.route> => React.element
Somewhere wanted:
  SomeLib.screenComponent (defined as SomeLib.screenProps => Jsx.element)

The incompatible parts:
  SomeComponent.props<SomeLib.navigation, SomeLib.route> vs
  SomeLib.screenProps

So in order to be able to use the API, I am forced to avoid the @react.component attribute and instead define my component like this:

module SomeComponent = {
  type props = SomeLib.screenProps
  let make = ({navigation: _, route: _}: props) => React.string("Some component")
}
2 Likes

I’m not used to React Navigation now. It’s been a while to use. How about using module function?

module SomeLib = (
  T: {
    type props<'navigation, 'route> = {
      navigation: 'navigation,
      route: 'route,
    }
    let make: React.component<props<_>>
  },
) => {
  type navigation
  type route

  type screenProps = T.props<navigation, route>

  type screenComponent = React.component<screenProps>

  module Screen = {
    @react.component @module("some-lib")
    external make: (~component: screenComponent) => React.element = "Screen"
  }
}

module SomeComponent = {
@react.component
  let make = (~navigation, ~route) => {
    let _ = (navigation, route)
    React.string("Some component")
}}

module SomeComp = SomeLib(SomeComponent)

let render = () => <SomeComp.Screen component=SomeComponent.make />

EDIT: surely, you can use @react.component either.

Yes, it’s definitely a nominal type effect :smiley:

I noticed createRoot is scoped inside Experimental. It does not look like React considers this API to be experimental; should we move it outside of Experimental?

1 Like

Good point! ReactDOM apis need to be cleaned up since bindings are having peer dependences>=React@18.x. It needs to be separated to react-dom/client and server too.

Cleanup of the bindings for React 18 is already done in https://github.com/rescript-lang/rescript-react/pull/46 which will be merged soon (feedback welcome).

5 Likes

Using first-class modules with locally abstract types does not seem to work.

module Select = {
  @react.component
  let make = (
    type a key,
    ~model as module(T: T with type t = a and type key = key),
    ~selected: option<key>,
    ~onChange: option<key> => unit,
    ~items: array<a>,
  ) =>
  ...
}

yields the error:

  The type of this packed module contains variables:
module(T with type key = 'type-key with type t = 'type-a)

Thank you for reporting!
This seems not compiled in v3 either. Can you give me a self-contained example?

module Select = {
  @react.component
  let make = (
    type a key,
    ~model as module(T: T with type t = a and type key = key), // Unbound module type T
    ~selected: option<key>,
    ~onChange: option<key> => unit,
    ~items: array<a>,
  ) => {
    <div />
  }
}

Sorry, it just needs some module type T with types t and key. This compiles in v3:

module Select = {
  module type T = {
    type key
    type t
  }

  @react.component
  let make = (
    type a key,
    ~model as module(T: T with type t = a and type key = key),
    ~selected: option<key>,
    ~onChange: option<key> => unit,
    ~items: array<a>,
  ) => {
    <div />
  }
}

Thank you! I found what needs to be done. Let you know once it’s fixed.

2 Likes

Thank you for the suggestion! It does work, however, the API becomes rather unwieldy this way as you would have to apply that SomeLib functor separately to every screen component you would like to use.

With React Navigation, it would become extra unwieldy as the API is already quite complex and already requires functors now.

1 Like

@glennsl I’ve fixed for the implementation. Can you give me an interface too? If you can, I’ll check the interface usage.

1 Like

The interface just uses ordinary unification variables, so i think it should be fine. It’d be something like this:

module Select: {
  module type T = {
    type key
    type t
  }

  @react.component
  let make: (
    ~model: module(T with type t = 'a and type key = 'key),
    ~selected: option<'key>,
    ~onChange: option<'key> => unit,
    ~items: array<'a>,
  ) => React.element
}
1 Like

The PR to fix is on review. https://github.com/rescript-lang/syntax/pull/666

1 Like

It looks like there may be an issue when not using the transform and using the external keyword.

type props<'msg> = {msg: 'msg}
@module("@foo/bar")
external make: props<_> => React.element = "SomeComponent"

Gets compiled to

JsxRuntime.jsx(
  (function (prim) {
    return Bar.SomeComponent(prim);
  }),
  { msg: "test" }
)

instead of

JsxRuntime.jsx(Bar.SomeComponent, {
  msg: "test"
})

Thanks for reporting.
This code seems an interface. Can you give me a sample example that is compiled?
Confused. I’ll look into it.