npm package: @fattafatta/rescript-solidjs
Description: ReScript bindings for solid-js.
Author: Fattafatta
Latest version: 0.6.0

# rescript-solidjs

## Note

This library is in **experimental** state. A lot of stuff is untested and some of the bindings may simply not work. Feedback is always welcome.
Previous versions used `HyperScript` to make `solidJs` work with `ReScript`. This is no longer recommended.

---

`rescript-solidjs` allows the use of [solidJs](https://github.com/solidjs/solid) with [ReScript](https://rescript-lang.org/) while still using `JSX`.

This library consists of two parts

1. It provides bindings for most of the `solidJs` feature set (see list of missing stuff below). The bindings are as close as possible to the original solidJs naming. In some cases a direct translation wasn't possible. Any deviations are listed in the documentation below.
2. It provides the necessary types to use `ReScript` without `rescript-react`. Also some types vary slightly between `rescript-react` and `solidJs`, which makes it impossible to use them together.

Also `ReScript` does not support `solidJs` natively. A workaround has to be used in order to make them work together. See details below.

## Background

Normally, `ReScript` compiles `JSX` directly to `JavaScript`. Therefore it is incompatible with `solidJs` since it expects the `JSX` to still be intact and uses their own compiler. Until this is changed (github issue to preserve JSX is already open: <https://github.com/rescript-lang/syntax/issues/539>) a workaround is required.

Currently there are two solutions that work with this library

1. Use a babel transform to convert compiled ReScript code back to JSX (recommended)
2. Trick the ReScript compiler to actually load `HyperScript` code instead of the original `react` code with a fake react implementation

### Babel preset

This is the currently recommended way to use this library.

The idea is to use [babel](https://babeljs.io/) to transform compiled ReScript code back to `JSX`. This is done by a preset that runs multiple transforms to make the code generated by ReScript compatible with `solidJs` before running it through the `solidJs` compiler. The corresponding preset can be found here: <https://github.com/Fattafatta/babel-preset-rescript-solidjs>

### Fake react with HyperScript (not recommended)

`solidJs` supports its [own](https://github.com/solidjs/solid/tree/main/packages/solid/h) version of `HyperScript` which could be used together with `ReScript` and some additional bindings. But using `HyperScript` instead of `JSX` is not a great developer experience.

Normally it would be necessary to develop a ppx to modify the behavior of the `ReScript` compiler, but instead this library uses its own fake version of [rescript-react](https://github.com/rescript-lang/rescript-react) bindings to create the necessary bridge between the code generated by `ReScript` and the `HyperScript` expected from `solidJs`.

Basically the `React.createElement` function provided by the fake react is replaced by the `h` function from `HyperScript`. Most of the magic happens in the `src/react/hyper.js` file. The rest of the library consists of all the react types required by the `ReScript` compiler, and the actual `solidJs` bindings.

### Comparison of both approaches

| Feature | Babel transform | HyperScript |
| --- | --- | --- |
|Reactivity | The code generated by babel-transform behaves exactly like the original solidJs. | `HyperScript` requires to wrap every reactive prop or child in a function (`unit => 'value`). See also "Reactivity with HyperScript" below. The standard control flow components (like `For`) do not support this. So the components had to be reimplemented. |
| External Components | Supported | `HyperScript` components require special function syntax. Most external libraries that use components (like `solid-app-router`) do not support that. |
| Performance | Uses the optimized `solidJs` JSX compiler and supports tree-shaking. | Uses the unoptimized `solid-js/h` library. | 

## Installation

This library supports the new ReScript versions >= 10.1, but it is backwards-compatible with older versions. For ReScript 10.1 or higher, just install the library normally.

For older versions (< 10.1) additional `bsc-flags` have to be set (see below).

### Library

Install with `npm`:

```bash
npm install solid-js @fattafatta/rescript-solidjs
```

Or install with `yarn`:

```bash
yarn add solid-js @fattafatta/rescript-solidjs
```

Add `@fattafatta/rescript-solidjs` as a dependency to your `bsconfig.json`:

```json
"bs-dependencies": ["@fattafatta/rescript-solidjs"]
```

#### For ReScript < 10.1

Some additional compiler flags have to be set for older versions:

```json
"reason": { "react-jsx": 3 },
"bsc-flags": ["-open ReactV3", "-open SolidV3"],
"bs-dependencies": ["@fattafatta/rescript-solidjs"]
```
(See also: The [migration guide](https://rescript-lang.org/docs/react/latest/migrate-react#v3-compatible-mode) from ReScript)

### Babel preset

Using babel to transform ReScript output to SolidJS compatible code. To install the previous version with HyperScript, check the end of this README.

Install with `npm`:

```bash
npm install @fattafatta/babel-preset-rescript-solidjs --save-dev
```

Or install with `yarn`:

```bash
yarn add @fattafatta/babel-preset-rescript-solidjs --dev
```

Follow the instructions in the [README](https://github.com/Fattafatta/babel-preset-rescript-solidjs/blob/main/README.md) to configure `babel`.

## Usage

The namings of the bindings are as close as possible to the original `solidJs` names. In some cases some deviations were necessary to better fit the `ReScript` type system.

### Quick example

A simple counter component.
(Note: Building a counter component is actually very tricky in react. But in `solidJs` it's really straightforward and behaves exactly as expected.)

```rescript
@react.component
let make = () => {
  let (count, setCount) = Solid.createSignal(1, ())

  let timer = Js.Global.setInterval(() => {
    setCount(c => c + 1)
  }, 1000)

  Solid.onCleanup(() => Js.Global.clearInterval(timer))

  <div>
    {`Hello ReScripters! Counter: ${Js.Int.toString(count())}`->React.string}
    <button
      onClick={_ => {
        setCount(c => c - 3)
      }}>
      {"Decrease"->React.string}
    </button>
  </div>
}
```

### Reactivity

#### createSignal

The original `~options` argument is polymorphic. Use either the `#bool` or the `#fn` polymorphic variant to set them.

```rescript
// normal
let (count, setCount) = Solid.createSignal(1, ())

// with equality options
let (count, setCount) = Solid.createSignal(1, ~options=#bool({equals: false}), ())
// or with equality fn
let (count, setCount) = Solid.createSignal(1, ~options=#fn({equals: (prev, next) => prev == next}), ())
```

#### createEffect

```rescript
let (a, setA) = Solid.createSignal("initialValue", ());

// effect that depends on signal `a`
Solid.createEffect(() => Js.log(a()), ())

// effect with optional initial value
Solid.createEffect(prev => {
  Js.log(prev)
  prev + 1
}, ~value=1, ())
```

#### createMemo

Supports the same `~options` as `createSignal`. `createMemo` passes the result of the previous execution as a parameter. When the previous value is not required use `createMemoUnit` instead.

```rescript
let value = Solid.createMemo((prev) => computeValue(a(), prev), ());

// set an initial value
let value = Solid.createMemo((prev) => computeValue(a(), prev), ~value=1, ());

// with options
let value = Solid.createMemo((prev) => computeValue(a(), prev), ~options=#bool({equals: false}), ());

// with unit function
let value = Solid.createMemoUnit(() => computeValue(a(), b()), ());
```

#### createResource

Originally `createResource`'s first parameter is optional. To handle this with rescript `source` and `options` have to be passed as labeled arguments. Refetching only supports `bool` right now (no `unknown`).

```rescript
let fetch = (val, _) => {
  // return a promise
}

// without source
let (data, actions) = Solid.Resource.make(fetch, ())
// with source
let (data, actions) = Solid.Resource.make(~source=() => "", fetch, ())
// with options
let (data, actions) = Solid.Resource.make(~source=() => "", fetch, ~options={initialValue: "init"} ())
// with initialValue. No explicit handling of option<> type necessary for data()
let (data, actions) = Solid.Resource.makeWithInitial(~source=() => "", fetch, ~options={initialValue: "init"} ())
```

### Events (e.g. onClick)

Solid offers an optimized array-based alternative to adding normal event listeners. In order to support this syntax a wrapper function `Event.asArray` has to be used.

```rescript
// solid's special array syntax
<button onClick={Solid.Event.asArray((s => Js.log(s), "Hello"))}>
  {"Click Me!"->React.string}
</button>

// normal event syntax
<button  onClick={e => Js.log("Hello")}>
  {"Click Me!"->React.string}
</button>
```

### Lifecycles

All lifecycle functions are supported.

### Reactive Utilities

Most utilities are supported.

#### mergeProps

ReScript does not offer the same flexibility for structural types as TypeScript does. The `mergeProps` function accepts any type without complaint, but it only works with records and objects. Also the compiler will have a hard time figuring out the correct type of the return value.

It is very easy to build breakable code with this function. Use with caution!

```rescript
type first = {first: int}
type second = {second: string}
let merged = Solid.mergeProps({first: 1}, {second: ""})
```

#### splitProps

Supported but untested. The original function expects an arbitrary number of parameters. In `ReScript` we have different functions `splitPropsN` to model that.

This function also easily breaks your code if used incorrectly!

```rescript
let split = Solid.splitProps2({first: 1, second: ""}, ["first"], ["second"])
```

### Stores

The `createStore` function is called `Solid.Store.make`, since this is a more idiomatic naming for `ReScript`.

```rescript
let (state, setState) = Solid.Store.make({greeting: "Hello"})
```

Solid's setState supports numerous practical ways to update the state. Since the function is so overloaded it is very hard to create bindings for it. Currently only the basic function syntax is supported.

```rescript
setState(state => {greeting: state.greeting ++ "!"})
```

#### unwrap

```rescript
let untracked = Solid.Store.unwrap(state)
```

### Component APIs

All Component APIs are supported.

#### lazy

Getting dynamic imports to work with ReScript is tricky, since ReScript works completely without explicit import statements. For it to work, the `"in-source": true` option in `bsconfig.json` should be used and the generated `bs.js` file needs to be referenced within the import.

The `Solid.Lazy.make` function returns a component, that requires to be wrapped in a `module`. Note that this can only be used inside a function (or component) and not on the top level of a file.

Currently only components without any props can be imported.

```rescript
@react.component
let make = () => {
  let module(Comp) = Solid.Lazy.make(() => Solid.import_("./Component.bs.js"))
  <Solid.Suspense fallback={"Loading..."->React.string}><Comp /></Solid.Suspense>
}
```

### Context

`createContext` always requires a defaultValue. Also ReScript requires all components to start with an uppercase letter, but the object returned by `createContext` requires lowercase. In order to create the `Provider` component `module(Provider)` has to be used.

```rescript
let context = Solid.Context.make((() => "", _ => ()))

module TextProvider = {
  @react.component
  let make = (~children) => {
    let module(Provider) = context.provider
    
    let signal = Solid.createSignal("initial", ())

    <Provider value={signal}> {children} </Provider>
  }
}
module Nested = {
  @react.component
  let make = () => {
    let (get, set) = Solid.Context.useContext(context)
    set(p => p ++ "!")
    <div> {get()->React.string} </div>
  }
}

@react.component
let make = () => <TextProvider><Nested /></TextProvider>
```

### Secondary Primitives

All are supported. `createSelector` is untested.

### createReaction

```rescript
let (get, set) = Solid.createSignal("start", ())
let track = Solid.createReaction(() => Js.log("something"))
track(() => get()->ignore)
```

### Rendering

`render` is working. All other functions are completely untested und might not work.

#### render

Attaches the root component to the DOM.

```rescript
Solid.render(() => <App />, Document.querySelector("#root")->Belt.Option.getExn, ())

// or with dispose
let dispose = Solid.render(() => <App />, Document.querySelector("#root")->Belt.Option.getExn)
```

#### DEV

Is named `dev` in rescript, and treated as `bool`.

### Control Flow

These are the regular bindings for the babel-transform variant. The HyperScript variants have their own module `Solid.H` (see below).

#### For

```rescript
<Solid.For each={["Arya", "Jon", "Brandon"]} fallback={<div> {"Loading..."->React.string} </div>}>
  {(item, _) => <div> {`${item} Stark`->React.string} </div>}
</Solid.For>
```

#### Show

SolidJs' `Show` can be used with any truthy or falsy (like `null`) value. The concept of a truthy value does not translate well to `ReScript`, so instead `Show` expects an `option<'t>`.

```rescript
<Solid.Show.Option \"when"={Some({"greeting": "Hello!"})} fallback={<div> {"Loading..."->React.string} </div>}>
  {item => <div> {item["greeting"]->React.string} </div>}
</Solid.Show.Option>
```

In those cases where the `when` clause contains an actual `bool` a different version of `Show` has to be used:

```rescript
<Solid.Show.Bool \"when"={something > 0} fallback={<div> {"Loading..."->React.string} </div>}>
   <div> {"Hello!"->React.string} </div>
</Solid.Show.Bool>
```

#### Index

```rescript
<Solid.Index each={["Arya", "Jon", "Brandon"]} fallback={<div> {"Loading..."->React.string} </div>}>
  {(item, _) => <div> {`${item()} Stark`->React.string} </div>}
</Solid.Index>
```

#### Switch/Match

`Match` supports the same Variants (`Bool`, `Option`) as `Show`.

```rescript
<Solid.Switch fallback={"Fallback"->React.string}>
  <Solid.Match.Bool \"when"={false}>
    {"First match"->React.string} 
  </Solid.Match.Bool>
  <Solid.Match.Option \"when"={Some("Second match")}>
    {text => text->React.string}
  </Solid.Match.Option>
</Solid.Switch>
```

#### ErrorBoundary

Only the variant with a fallback function is supported.

```rescript
<Solid.ErrorBoundary fallback={(_, _) => <div> {"Something went terribly wrong"->React.string} </div>}>
  <MyComp />
</Solid.ErrorBoundary>
```

#### Suspense

```rescript
<Solid.Suspense fallback={<div> {"Loading..."->React.string} </div>}> <AsyncComp /> </Solid.Suspense>
```

### Special JSX Attributes

Custom directives are not supported.

#### ref

Refs require function syntax.

```rescript
@react.component
let make = () => {
  let myRef = ref(Js.Nullable.null)

  <div ref={el => {myRef := el}} />
}
```

#### classList

`classList` behaves differently. Instead of an object it uses tuples of `(string, bool)`. It uses a thin wrapper to convert the tuples into an object.

```rescript
<div classList={Solid.makeClassList([("first", val() == 0), ("other", val() != 0)])} />
```

#### style

`style` only supports string syntax right now.

```rescript
<div style={`background-color: green; height: 100px`} />
```

#### on...

See Events section above.

## Examples

Please check the `examples` folder for a complete project configured with `ReScript`, `solidJs` and `vite`.

## Missing features

For these features no bindings exist yet.

- observable
- from
- produce
- reconcile
- createMutable
- all stuff related to hydration is untested
- Dynamic
- custom directives
- /_ @once _/

## Usage of HyperScript variant

The first version of this library used HyperScript as bridge between `ReScript` and `solidJs`. Although the bindings for both variants are almost identical, there are two differences to note:

1. For HyperScript to be reactive, every prop and child has to be wrapped in a function.
2. `For`, `Show` and `Index` versions for HyperScript are in their own module (`Solid.H`)

### Installation

We have to trick `ReScript` to accept this library as a replacement for the original react bindings. This can be accomplished by using a module alias.

Install with `npm`:

```bash
npm install solid-js @fattafatta/rescript-solidjs react@npm:@fattafatta/rescript-solidjs
```

Or install with `yarn`:

```bash
yarn add solid-js @fattafatta/rescript-solidjs react@npm:@fattafatta/rescript-solidjs
```

Add `@fattafatta/rescript-solidjs` as a dependency to your `bsconfig.json`:

```json
"bs-dependencies": ["@fattafatta/rescript-solidjs"]
```

Make sure to **remove** `@rescript/react` if it is already listed. It is impossible to use this library and the original react binding together.

### Reactivity with HyperScript

`solidJs`' HyperScript requires that all reactive props and children are wrapped in a function (`unit => 'returnValue`). But adding those functions would completely mess up the `ReScript` type system. The solution is to wrap any reactive code with the `Solid.track()` function.
(This function adds no additional overhead and will be removed by the complier. It's only purpose is to make the types match.)

```rescript
// GOOD
{Solid.track(() => (count()->React.int))}

// BAD, this count would never update
{count()->React.int}
```

#### Control flow with HyperScript

The necessary HyperScript bindings for `Show`, `For` and `Index` are all encapsulated in the module `Solid.H`. These helper components always expect reactive syntax (e.g. props have to we wrapped in `() => 'a`). Therefore it is not necessary to wrap the `each` or `when` with a `track`.

Example for `For`:

```rescript
<Solid.H.For each={() => ["Arya", "Jon", "Brandon"]} fallback={<div> {"Loading..."->React.string} </div>}>
  {(item, _) => <div> {`${item} Stark`->React.string} </div>}
</Solid.H.For>
```

Example for `Show`:

```rescript
<Solid.H.Show.Option \"when"={() => Some({"greeting": "Hello!"})} fallback={<div> {"Loading..."->React.string} </div>}>
  {item => <div> {item["greeting"]->React.string} </div>}
</Solid.H.Show.Option>
```

## Acknowledgments

This library used multiple sources for inspiration. Especially <https://github.com/woeps/solidjs-rescript> was of great help to get the initial version going. It proved that `ReScript` and `solidJs` could work together when using `HyperScript`. The _only_ missing step was to make the `ReScript` compiler produce `HyperScript`, to that `JSX` would work too.

### Additional info

Discussion about `ReScript` on github:
<https://github.com/solidjs/solid/discussions/330#discussioncomment-339972>

Discussion about `solidJs` in the `ReScript` forums:
<https://forum.rescript-lang.org/t/change-jsx-output-from-react-to-solidjs/1930/12>