Skip to content

elias-michaias/rescript-chai

Repository files navigation

Chai Logo

rescript-chai

Elm     ReScript     React     zustand (bear)

The Elm Architecture - in ReScript - on React - with Zustand

GitHub top language npm version gzipped size GitHub last commit


Warning

Chai is currently in early development. Some APIs are incomplete, unstable, or subject to change. Do not use Chai in production.


Chai Code


What is Chai? 🍵

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.

Installation 🚀

npm install rescript-chai

Reference 📖

About

An implementation of The Elm Architecture (TEA) in ReScript - built on React - with Zustand.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors