[Feedback] Preserve JSX mode

When working with SolidJS, you always have to keep the basic reactivity concepts in mind, the interactions between signals, subscribers and tracking scope.

Signals provide getter and setter functions to work with data. If a getter is used within a tracking scope, all code within that scope will be rerun if the signal updates.

A subscriber basically defines a tracking scope. The most simple example would be the createEffect function. Any getter used within createEffect will “subscribe” to the corresponding signal and will automatically be updated (rerun) whenever the signal changes.

Any JSX component will also create its own tracking scope.

This has some interesting implications when working with SolidJS. For example the following code would not work:

function App() {
  const [maybe] = createSignal("hi");

  return (
      maybe() ? <div>{maybe()}</div> : null
  );
}

Even though the getter (maybe()) is used correctly (used twice), The first one is outside of any tracking scope. So the component will never update.

Just adding a fragment (<>) would fix this, because the fragment would create its own tracking scope.

function App() {
  const [maybe] = createSignal("hi");

  return (
      <>maybe() ? <div>{maybe()}</div> : null</>
  );
}

This is of course somewhat hacky, and therefore it is recommended to instead use the <Show> component.

function App() {
  const [maybe] = createSignal("hi");

  return (
    <Show when={maybe()}>
      <div>maybe()</div>
    </Show>)
}

The Show component will automatically generate a tracking scope and subscribe to all relevant signals.

While at first, this seems complicated, it is something that every experienced SolidJS programmer will know well, and they would easily spot errors like the one above.

The same would be true for this snippet (that would not work either):

function App() {
  const [maybe] = createSignal("hi");
  const m = maybe();

  return (
      <> m ? <div>{m}</div> : null</>
  );
}

It is easy to spot, that the getter is called outside a tracking scope. So no updates will be triggered.

The point I’m trying to make is that, although SolidJS can be complicated, it is also very explicit. In many cases the error is obvious and easy to spot, just by looking at the code.

Basically this line const m = maybe() would be a huge red flag.

Now let’s have a look at ReScript.

@react.component
let make = () => {
  let maybe = createSignal(Some("hi"))

  switch maybe() {
    | Some(m) => <div>{m->React.string}</div>
    | _ => React.null
  }
}

I would say that for the average ReScript programmer this would look like valid code. A SolidJS programmer, coming to ReScript, would probably think the same. (Although they might be irritated by the use of a getter inside the switch statement.)

But in order to get the error right away, you need a decent understanding of how the compiler transforms the code to JavaScript.

The problem only becomes obvious when looking at the generated JS code. The getter is clearly called outside a tracking scope:

function App() {
  let maybe = SolidJs.createSignal("hi");
  let m = maybe();
  if (m !== undefined) {
    return <div>
    {m}
    </div>;
  } else {
    return null;
  }
}

The problem also persists if using the “fragment trick” in the TypeScript example. Adding a fragment would result in this code:

function Broken(props) {
  let maybe = SolidJs.createSignal("hi");
  let m = maybe();
  return <>
  {Primitive_option.some(m !== undefined ? <div>
    {m}
    </div> : null)}
  </>;
}

That’s why the problem is much more relevant for ReScript than it is for TypeScript. Those transformations and small optimizations that the ReScript compiler does, can lead to problems that would not happen in TypeScript.
And those changes are hidden when only looking at the ReScript code.
You have to know how a ReScript switch statement is represented in JavaScript, in order to be able to spot the problem.

So what I’m trying to say is, that neither a ReScript programmer coming to SolidJS nor a SolidJS programmer coming to ReScript, will have the necessary understanding and intuitions about the code. Because it is necessary to have a good understanding of the ReScript compiler and of reactivity in SolidJS.
This can easily lead to frustration and the believe that SolidJS and ReScript do not work together.

(Honestly it would be interesting to know, how many ReScript programmers actually check the resulting JS code. My assumption is that most people do, but I’m not sure about it).

But those small optimizations are one of the great features of ReScript (at least for me), that make it so much easier to write good code without having to think too much about it.
Only in this very specific case, when using SolidJS, it sadly makes it more complicated.

So, from my perspective, solving those problems should not be done by changing the way the ReScript compiler behaves. It is more about finding a good way to make those interactions more explicit and easier to spot.

My first step would be to just add a little more documentation and simply explain those interactions. That would make it much easier for a newcomer, to avoid those mistakes.

Thinking about it. When I update rescript-solidjs for ReScript V12, I will probably just add some explanations in the documentation. That should already be a great improvement.

2 Likes

Thank you for the thorough write up! It would be interesting to think about whether there’s anything additional we can do to make the clearer/easier to work with. I look forward to seeing how things look with v12 and rescript-solidjs!

1 Like