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 with ReScript while still using JSX
.
This library consists of two parts
- 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. - It provides the necessary types to use
ReScript
withoutrescript-react
. Also some types vary slightly betweenrescript-react
andsolidJs
, 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
- Use a babel transform to convert compiled ReScript code back to JSX (recommended)
- Trick the ReScript compiler to actually load
HyperScript
code instead of the originalreact
code with a fake react implementation
Babel preset
This is the currently recommended way to use this library.
The idea is to use babel 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 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 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
:
npm install solid-js @fattafatta/rescript-solidjs
Or install with yarn
:
yarn add solid-js @fattafatta/rescript-solidjs
Add @fattafatta/rescript-solidjs
as a dependency to your bsconfig.json
:
"bs-dependencies": ["@fattafatta/rescript-solidjs"]
For ReScript < 10.1
Some additional compiler flags have to be set for older versions:
"reason": { "react-jsx": 3 },
"bsc-flags": ["-open ReactV3", "-open SolidV3"],
"bs-dependencies": ["@fattafatta/rescript-solidjs"]
(See also: The migration guide 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
:
npm install @fattafatta/babel-preset-rescript-solidjs --save-dev
Or install with yarn
:
yarn add @fattafatta/babel-preset-rescript-solidjs --dev
Follow the instructions in the README 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.)
@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.
// 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
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.
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
).
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.
// 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!
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!
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
.
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.
setState(state => {greeting: state.greeting ++ "!"})
unwrap
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.
@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.
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
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.
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
<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>
.
<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:
<Solid.Show.Bool \"when"={something > 0} fallback={<div> {"Loading..."->React.string} </div>}>
<div> {"Hello!"->React.string} </div>
</Solid.Show.Bool>
Index
<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
.
<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.
<Solid.ErrorBoundary fallback={(_, _) => <div> {"Something went terribly wrong"->React.string} </div>}>
<MyComp />
</Solid.ErrorBoundary>
Suspense
<Solid.Suspense fallback={<div> {"Loading..."->React.string} </div>}> <AsyncComp /> </Solid.Suspense>
Special JSX Attributes
Custom directives are not supported.
ref
Refs require function syntax.
@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.
<div classList={Solid.makeClassList([("first", val() == 0), ("other", val() != 0)])} />
style
style
only supports string syntax right now.
<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:
- For HyperScript to be reactive, every prop and child has to be wrapped in a function.
-
For
,Show
andIndex
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
:
npm install solid-js @fattafatta/rescript-solidjs react@npm:@fattafatta/rescript-solidjs
Or install with yarn
:
yarn add solid-js @fattafatta/rescript-solidjs react@npm:@fattafatta/rescript-solidjs
Add @fattafatta/rescript-solidjs
as a dependency to your bsconfig.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.)
// 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
:
<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
:
<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