The Elm Architecture - in ReScript - on React - with Zustand
Warning
Chai is currently in early development. Some APIs are incomplete, unstable, or subject to change. Do not use Chai in production.
Chai is an implementation of The Elm Architecture (TEA) in ReScript - built on React and zustand 🐻. Chai wants to make the React ecosystem accessible to the Model-View-Update paradigm, without sacrificing on the comforts you're used to. Model your state, clearly define all state transformations, and represent side effects as data structures.
Here's an example of a file Brew.res that defines the core logic for a Model-View-Update loop:
// Brew.res
// Define your model - the state of your component
type model = { count: int }
// Define your messages - events that can change state
type msg = Increment | Decrement | Set(int)
// Define your commands - effects that interact with the world
type cmd = NoOp | Log(Cmd.Log.t)
// The update function - pure, handles all state changes
let update = (model, msg) => switch msg {
| Increment => ({ count: model.count + 1 }, NoOp)
| Decrement => ({ count: model.count - 1 }, NoOp)
| Set(n) => ({ count: n }, Log("Set count to" ++ string_of_int(n)))
}
// Handle your side effects (HTTP, storage, timers, etc.)
// Delegate to Chai runtime defaults (Cmd.Log.run) or make your own
// (Swap out runners for test and production environments!)
let run = async (cmd, dispatch) => switch cmd {
| NoOp => ()
| Log(c) => await c->Cmd.Log.run
}
// Subscriptions for external events (WebSocket, timers, etc.)
let subs = (model) => [
// while condition true, do ...
Sub.Time.every(model.count < 30, {
interval: 1000,
cons: _ => Increment,
})
]
// Describe the initial state and effects
let init = (
{
count: 0
},
Log("Counter initialized")
)
// Use zustand middleware or make your own!
// Compatible with redux devtools :)
// https://github.com/reduxjs/redux-devtools
let middleware = (store) => store
->Chai.persist({name: "counter"})
->Chai.devtools({})
// Wire everything together!
let useCounter = Chai.brew({
update, run, subs, init, middleware, opts: {
// Undo/Redo/Reset functionality
chrono: { enabled: true }
}
})After you've created your hook, you can then use it anywhere you want. The generated hook is idempotent and will never re-run effects. You can safely call it from any component to access the core loop's state:
// Counter.res
open Chai
@react.component
let make = () => {
// `useCounter` is idempotent -
// use this hook anywhere to tap into the core MVU loop
// without fear of re-running effects
let (state, dispatch, _) = Brew.useCounter()
<div>
<h2>{React.string(state.title)}</h2>
<p>{React.string("Count: " ++ Int.toString(state.count))}</p>
<button onClick={_ => Increment->dispatch}>
{React.string("Inc")}
</button>
</div>
}Because Chai uses Zustand under the hood, granular reactivity has never been easier. select delegates to Zustand and only re-renders when the selected projection changes.
npm install rescript-chai
