Skip to content

Assessment: native solidJS support in rescript #14

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

Open
zth opened this issue Feb 24, 2023 · 7 comments
Open

Assessment: native solidJS support in rescript #14

zth opened this issue Feb 24, 2023 · 7 comments

Comments

@zth
Copy link

zth commented Feb 24, 2023

Just wanted to say that this is a cool project!

I'm wondering, could you at some point do a renewed assessment of what'd be needed from the ReScript side to support SolidJS more "natively", now with JSX v4 etc released?

I understand we need a "preserve" mode for JSX. Is there more things needed, like swapping out JSX factory functions etc in a better way? And if so, has that been (partly) solved by JSX v4?

I'm very interested in what we can do to make SolidJS and ReScript a great combination. It feels like they fit each other quite well.

@Fattafatta
Copy link
Owner

Hi @zth, thanks!

currently (with version v3) the solidjs integration faces two issues.

The first one is the mentioned "preserve" mode that needs to be implemented by the compiler. Here is the corresponding ticket for rescript: rescript-lang/syntax#539

The second one is that the ReScript compiler breaks reactivity by reassigning component properties to variables.
Here is an example on how this looks like:

// rescript
module Comp = {
  @react.component
  let make = (~text) => {
    
    <div> {React.string(text)} </div>
  }
}

// resulting javascript
function Playground$Comp(Props) {
  var text = Props.text;
  return React.createElement("div", undefined, text);
}

Playground: https://rescript-lang.org/try?version=v10.1.2&code=ALBWGcA8GEHsDsBmBLA5gCgN4DcCmAncZBALgAIBmAGjIFtYATXcgIgGMAbAQ3CLZYC+ASgBQI+gwCuHXGTi0ADmQC8ZTCLJlg+XFzYAXAHRtYihLnj6NZGfrpcA1rNXoAfvtyR9QlQD411pqBZAA8DMjY-pgASroGhuD6+MjwGB5eQgKhAPThkdYCIoVAA

I just checked the playground with the new version v4 and it seems that the problem with props reassignment is no longer present. So that's good news. Now we only really need the JSX preserve mode.

If I remember correctly, they also introduced props spread with the new version. I have to check out, how this is implemented too. Hopefully this is not an issue.

@Fattafatta
Copy link
Owner

I made some more tests and I found some scenarios, where the problem with the prob reassignment is still present. In the older version, all props have been reassigned (i.e. var text = Props.text;). Now this only happens when the property is used in more than one place:

// rescript
module TwoProps = {
  @react.component
  let make = (~className, ~height) => {
    <div className height> 
    	<div className height />
    </div>
   
  }
}

// resulting javascript
function Playground$TwoProps(props) {
  var height = props.height;
  var className = props.className;
  return JsxRuntime.jsx("div", {
              children: JsxRuntime.jsx("div", {
                    className: className,
                    height: height
                  }),
              className: className,
              height: height
            });
}

Playground

From a solidjs perspective this is problematic, since this behaviour would break reactivity of props.
So sadly a simple "preserve" mode alone would currently not be enough to make solid work 100%.

It would be interesting to know why the compiler behaviour changed for "single-use-props", but not in every case. The current props handling would result in minimally shorter code. But that is such a small optimisation that perhaps it would be worth dropping it, in favour of supporting reactive libraries like solidjs.

@Fattafatta Fattafatta changed the title Cool project Assessment: native solidJS support in rescript Mar 12, 2023
@Fattafatta
Copy link
Owner

I found an additional case where the behaviour of the rescript compiler interferes with the reactivity model of solidJS.

When a thunk (function with no argument) returns an optional value (option<>), and is accessed inside a switch statement, the compiler will store the result of that function inside a variable and only used the variable afterwards:

I'm using a slightly simplified version of createSignal here. But it shows how reactivity would break for this signal (i.e. the div would never update even when the value of maybe changes.)

@module("solid-js")
external createSignal:'value => (unit => 'value) = "createSignal"

@react.component
let make = () => {
  let maybe = createSignal(Some("option"))
  
  <div>
  	{
      switch maybe() {
  	  | Some(m) => m
      | _ => ""
  	  }->React.string
  	}
  </div>
}

And here the compiled output:

function Playground(props) {
  var partial_arg = "option";
  var maybe = function (param) {
    return SolidJs.createSignal(partial_arg);
  };
  var m = Curry._1(maybe, undefined);
  return JsxRuntime.jsx("div", {
              children: m !== undefined ? m : ""
            });
}

Playground

The problematic part is:

var m = Curry._1(maybe, undefined);

The variable is assigned outside the tracking scope of solidJS and as a result, the component never receives any updates.

@Fattafatta
Copy link
Owner

Fattafatta commented Apr 13, 2023

I found another potential issue with the ReScript compiler behaviour and solidJS. The compiler produces slightly different results for a manually created component. The major part being, that the variable name of the custom component is lowercase.

Here is a Playground example with the full code: Playground

The relevant parts:

// custom component
module M1 = {
  type props<'msg> = {msg: 'msg}
  let make = props => React.string(props.msg)
}

// standard component
module M2 = {
  @react.component
  let make = (~msg) => React.string(msg)
}

And the resulting JavaScript

// custom component
function make(props) {
  return props.msg;
}

// standard component created with @react.component
function Playground$M2(props) {
  return props.msg;
}

Those snippets, while functionally identical will be compiled differently in solidJS.

After babel transform the code looks like this (again, only the relevant part):

function Playground(props) {
  return <><make msg="hi" /><Playground$M2 msg="hi" /></>;
}

And this will be transformed by solidJS like this:

const _tmpl$ = /*#__PURE__*/_$template(`<make msg="hi"></make>`, 2);
function Playground(props) {
  return [_tmpl$.cloneNode(true), _$createComponent(Playground$M2, {
    msg: "hi"
  })];
}

Basically the custom component will be turned into a template and only the standard component will be converted into an actual solid component.
The reason for that is (at least that is my assumption) that solid treats JSX elements that are all lowercase as built-in elements (like <div> or <p>).

This is a problem for the current solution with the babel transform.

So when implementing the JSX preserve mode it is important to make sure, that component names always start with a capital letter.
My assumption is, that this will automatically be the case (since react has the exact same behaviour). But I mention it here to make sure.

@unoexperto
Copy link

@Fattafatta Sorry for ping. What do you think about compatibility today?

@zth
Copy link
Author

zth commented May 9, 2025

ReScript v12 will ship with preserve mode, which means that the JSX part of the issues of using Solid should be solved.

Here's some investigation for what might now work and/or might still be broken for SolidJS to work flawlessly.

Props destructuring and component names - ✅ ?

I haven't tested thoroughly, but the @jsx.componentWithProps annotation seems to preserve the prop dot access from the main props object when I try it. It also preserves the component name as uppercase:

type props = {name: string}

module SolidComponent = {
  @jsx.componentWithProps(: props)
  let make = props => {
    <div> {React.string(props.name)} </div>
  }
}

Ignore the React. part and that I didn't turn on preserve mode just yet:

function Playground$SolidComponent(props) {
  return JsxRuntime.jsx("div", {
    children: props.name
  });
}

The props.name access is preserved.

Intermediate values created when switching - ✅ ?

When a thunk (function with no argument) returns an optional value (option<>), and is accessed inside a switch statement, the compiler will store the result of that function inside a variable and only used the variable afterwards:

Isn't this the same in TypeScript? The equivalent in TS would be using a ternary, but you'd still need to call maybe() again in the ternary success branch.

The ReScript equivalent would be that you'd call maybe() again in the success branch of the switch. That produces no intermediate value. This would mean your type is no longer refined and you'll need to unsafely unwrap it, but I don't see how you wouldn't also need to do that in TS...?

Seems SolidJS is proposing using <For>, <If>, <Show> etc to handle these cases.

Summing up

Summing this up, I think we should have what we need to make this work in v12...? You'll need to follow a few principles to not make this break. Most of them are the same in TS, but some are ReScript specific. Enforcing those principles is probably something we can look at doing centrally for Solid if it proves to be something people want to use.

I guess the next step would be to try SolidJS again in 2025, with v12 preserve mode, and see if it works now in practice.

@jmagaram
Copy link

jmagaram commented May 9, 2025

I tried this awhile ago. I remember there were problems with gentype making tsx files that broke reactivity. If gentype creates .d.ts codeless output that problem would go away.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants