Even more JSX customization

While experimenting with mithriljs, I was curious if I could use it in rescript (with jsx support) as well.

So I started a new project, added a JSX module and implemented all necessary types and functions.

Example of a mithril component in js:

const Component = (vnode) => {
  console.log(vnode.attrs.message);

  return {
    view: vnode => <div>{vnode.attrs.message}</div>
  };
};

I encountered some problems, but its difficult to explain or to show code because I tried a lot. But here are some of them:

  • (By default) rescript expects components to be functions with a single parameter (props) which will return jsx.
    • With some type gymnastics, I can write my components, but I can’t force to make all components have this shape.
    • @jsx.component / @jsx.componentWithProps don’t work on different shapes.
  • Jsx.elements can’t have a type parameter, so it’s not easy to type the vnode parameter to have the same type as in the view function.
    (type rec element<'props> = {view: 'props => element<'props>})

Here is my current rescript solution. It compiles, but

  • I could also define components, which just return jsx and they will crash at runtime.
  • I have to specify the types of my make functions explicitly or I need these helper functions.
  • I need an empty record type for components without props. Otherwise I get this error: “Empty record literal {} should be type annotated or used in a record context.”.
// M.res
type noProps = {}
type vnode<'props> = {attrs: 'props}
type comp<'props> = {view: vnode<'props> => Jsx.element}

let component = (options: vnode<noProps> => comp<noProps>): (noProps => Jsx.element) => {
  Obj.magic(options)
}

let componentP = (options: vnode<'props> => comp<'props>): ('props => Jsx.element) => {
  Obj.magic(options)
}
// Counter.res
type props = {initialCount: int}

let make = M.componentP(vnode => {
  let count = ref(vnode.attrs.initialCount)

  {
    view: _vnode => {
      <div>
        <button onClick={_ => count.contents = count.contents + 1}>
          {Jsx.string("click me")}
        </button>
        <span> {Jsx.int(count.contents)} </span>
      </div>
    },
  }
})

TLDR;

It’s just an experiment. I don’t know, if I want to use mithril in any project. But I had similar problems when I was implementing some web component stuff. So my question:

What is the future of JSX in rescript?
Do you think it is feature complete?
Are the improvements in mind (looking at custom data attributes)?

Edit: Just tripped over this issue: Make `Jsx.component` abstract ¡ Issue #8072 ¡ rescript-lang/rescript ¡ GitHub

2 Likes

Indeed, JSX element doesn’t have a type parameter, I wonder what we can do about this here.

About the single parameter of JSX components, I’m not sure to follow, do you have examples of such components that don’t follow this rule? How are they used?

JSX in rescript is definitely not considered as feature complete.

Making JSX components abstract is one direction of work.

We’re also thinking about enabling text in JSX children as in JS.

But if you feel like other things are missing, it’s definitely worth exploring it as you’re doing!

1 Like

About the single parameter of JSX components, I’m not sure to follow, do you have examples of such components that don’t follow this rule? How are they used?

The type of a component (in rescript) is always something like this: props => Jsx.element

In mithril for example the type would look like this: vnode => mithrilComponent
Where the so called props (e.g. in react) are just a sub property (attrs) of vnode.

<Counter initialValue=10 /> ← the compiler has to know, that the initialValue comes from “vnode.attrs” not from “props”.

// js
const Counter = vnode => {
    let count = vnode.attrs.initialValue;

    return {
        view: _ => (
            <div>
                <button onclick={_ => { count++ }}>click me</button>
                {count}
            </div>
        ),
    };
}

The rescript implementation will fail.

// res
let make = vnode => {
    let count = ref(vnode.attrs.initialValue)

    {
        view: _ => {
            <div>
                <button onClick={_ => count.contents = count.contents + 1}>
                    {Jsx.string("click me")}
                </button>
                {Jsx.int(count.contents)}
            </div>
        },
    }
}

While I can cast it with my component / componentP functions (s. previous post), the type system wouldn’t complain about react like implementations and if the attrs type is generic, it will throw this error “This expression’s type contains type variables that cannot be generalized: ‘_weak1 => Jsx.element’”.

Example:

type props<'a> = {
  options: array<'a>,
  getOptionId: 'a => string,
  getOptionLabel: 'a => string,
}

let make = M.componentP(_ => {
  view: vnode => {
    <ul>
    </ul>
  },
})

Additionally the “hyperscript” function type (for native elements) looks like this in mithril:

@module("mithril") external m: (string, domProps, option<element>) => element = "m"

So I have to normalize the props before:

let createElement = (tag: string, props: domProps) => {
  let (props, children) = normalize(props)

  m(tag, props, children)
}

Would be nice if you could specify, how the jsx code will be transformed.

Ok I see the issue now, then I guess the ppx transformation has to be different, you’d have to provide an ad-hoc ppx I’m afraid!

I completely agree! The lack of sensible and convenient data-attributes handling in rescript-jsx completely disqualifies this rescript feature from commercial use ;(