The Elm Architecture (TEA) for ReScript, providing a principled way to build web applications with guaranteed state consistency, exhaustive event handling, and time-travel debugging.
rescript-tea implements The Elm Architecture pattern for ReScript with React integration. It provides:
-
Single source of truth - One model, one update pathway
-
Pure updates - Easy to test, easy to reason about
-
Type-safe - Compiler catches missing message handlers
-
React integration - Uses React for rendering, compatible with existing components
-
Commands & Subscriptions - Declarative side effects
npm install rescript-teaAdd to your rescript.json:
{
"bs-dependencies": ["rescript-tea", "@rescript/react"]
}open Tea
// 1. Define your model
type model = {count: int}
// 2. Define your messages
type msg =
| Increment
| Decrement
// 3. Initialize your app
let init = () => ({count: 0}, Cmd.none)
// 4. Handle updates
let update = (msg, model) => {
switch msg {
| Increment => ({count: model.count + 1}, Cmd.none)
| Decrement => ({count: model.count - 1}, Cmd.none)
}
}
// 5. Render your view (with dispatch for event handling)
let view = (model, dispatch) => {
<div>
<button onClick={_ => dispatch(Decrement)}> {React.string("-")} </button>
<span> {model.count->Belt.Int.toString->React.string} </span>
<button onClick={_ => dispatch(Increment)}> {React.string("+")} </button>
</div>
}
// 6. Declare subscriptions (none for this simple example)
let subscriptions = _model => Sub.none
// 7. Create the app component
module App = MakeWithDispatch({
type model = model
type msg = msg
type flags = unit
let init = _ => init()
let update = update
let view = view
let subscriptions = subscriptions
})
// 8. Mount it
switch ReactDOM.querySelector("#root") {
| Some(root) => {
let rootElement = ReactDOM.Client.createRoot(root)
rootElement->ReactDOM.Client.Root.render(<App flags=() />)
}
| None => ()
}Your application state is a single value (typically a record):
type model = {
user: option<user>,
posts: array<post>,
loading: bool,
}All possible events are variants of a single type:
type msg =
| FetchPosts
| GotPosts(result<array<post>, error>)
| SelectPost(int)
| LogoutA pure function that handles messages:
let update = (msg, model) => {
switch msg {
| FetchPosts => (model, fetchPostsCmd)
| GotPosts(Ok(posts)) => ({...model, posts, loading: false}, Cmd.none)
| GotPosts(Error(_)) => ({...model, loading: false}, Cmd.none)
| SelectPost(id) => ({...model, selectedId: Some(id)}, Cmd.none)
| Logout => ({...model, user: None}, Cmd.none)
}
}Descriptions of side effects to perform:
// Do nothing
Cmd.none
// Batch multiple commands
Cmd.batch([cmd1, cmd2, cmd3])
// Perform an async operation
Cmd.perform(() => fetchUser("alice"), user => GotUser(user))
// Handle potential failures
Cmd.attempt(() => fetchUser("alice"), result => GotUser(result))Declarations of external event sources:
let subscriptions = model => {
if model.timerRunning {
Sub.Time.every(1000, time => Tick(time))
} else {
Sub.none
}
}Built-in subscriptions:
-
Sub.Time.every(ms, toMsg)- Timer -
Sub.Keyboard.downs(toMsg)- Key down events -
Sub.Keyboard.ups(toMsg)- Key up events -
Sub.Mouse.clicks(toMsg)- Mouse clicks -
Sub.Mouse.moves(toMsg)- Mouse movement -
Sub.Window.resizes(toMsg)- Window resize
Type-safe JSON decoding:
open Tea.Json
let userDecoder = map3(
(id, name, email) => {id, name, email},
field("id", int),
field("name", string),
field("email", string),
)
// Use it
switch decodeString(userDecoder, jsonString) {
| Ok(user) => // use user
| Error(err) => Console.log(errorToString(err))
}Optional HTML helpers (you can also use JSX directly):
open Tea.Html
let view = model => {
div([className("container")], [
h1([], [text("Hello")]),
button([onClick(Increment)], [text("+")]),
])
}rescript-tea uses React hooks internally to implement the TEA runtime:
-
useState - Stores the model state
-
useRef - Maintains cleanup functions for subscriptions
-
useEffect - Executes commands and manages subscription lifecycle
-
useCallback - Memoizes the dispatch function
This provides a seamless integration with React while maintaining TEA’s guarantees.
| Bug Type | How TEA Prevents It |
|---|---|
Stale UI |
View is pure function of Model |
Forgotten state updates |
View recomputes entirely |
Unhandled events |
Variant types = compiler warnings |
Race conditions |
Single update pathway |
Untestable code |
Pure functions = easy testing |
This project is dual-licensed under:
See CONTRIBUTING for details on how to contribute.
This repository follows the Rhodium Standard Repositories specification.