Bonsai

repository·master·Indexed 18 days ago

https://github.com/janestreet/bonsai

A collection of OCaml libraries for building reactive, incremental user interfaces for both browser-based web applications (Bonsai_web) and terminal-based interfaces (Bonsai_term). It treats components as purely functional state machines and provides tools for composable state, automated UI testing, and a Trampoline monad to prevent stack overflows in js_of_ocaml.

Tokens
2.1K
Snippets
6
Records
8
Agent score
74%

What's inside Bonsai

  1. Introduction to Bonsai

    master

    Bonsai is a UI library for building performant, reactive web applications in OCaml. It is inspired by Elm and is designed around the concept of components implemented as purely functional state machines.

    Key characteristics include:

    • Incrementality: Values (including the view) are only recomputed when necessary, preventing unnecessary work.
    • Composable State: Unlike frameworks that tie state to a UI component hierarchy, Bonsai allows you to compose state and incrementality primitives independently. This makes it easier to manage state lifecycles, such as embedding stateful components within tabbed interfaces without manual state hoisting.
    • Full-stack OCaml: Enables sharing types and business logic between the backend and frontend.
    module Dice = struct
      let faces =
        [ "⚀"; "⚁"; "⚂"; "⚃"; "⚄"; "⚅" ]
      ;;
    
      let component (graph @ local) =
        (* Components are implemented as purely functional state machines. *)
        let face, set_face = Bonsai.state (List.hd_exn faces) graph in
        (* Components are incrementally rendered, only when the relevant parts of the state change. *)
        let%arr face and set_face in
        {%html|
          <div>
            You rolled a #{face}
            <button
              style=""
              on_click=%{fun _ ->
                let index = Random.int (List.length faces) in
                set_face (List.nth_exn faces index)}
            >
              Roll the dice
            </button>
          </div>
        |}
      ";;
  2. Understand the Bonsai ecosystem

    master

    Bonsai is a collection of specialized libraries. The core Bonsai library provides the primitives for incremental, composable state machines, which are then specialized for different targets:

    Core

    • Bonsai: General-purpose incremental, composable state machines.
    • Bonsai_test: Testing utilities for the core library.

    Browser-based UI (Bonsai_web)

    Used for building interactive web applications.

    • Includes Bonsai_web_components, Bonsai_web_test, and Bonsai_bench.
    • Often uses ppx_html (JSX-like HTML) and ppx_css (CSS preprocessor).

    Terminal-based UI (Bonsai_term)

    Used for building interactive terminal applications.

    • Includes Bonsai_term_components and Bonsai_term_test.
  3. Prevent stack overflows in js_of_ocaml using Trampoline

    master

    Because js_of_ocaml (JSOO) lacks tail call optimization, deeply recursive functions can cause stack overflow errors. Trampoline is a monad designed to mitigate this by managing the execution flow to avoid deep recursion on the call stack.

    To use Trampoline, you must:

    1. Use the let%bind.Trampoline syntax (provided by the library's effect handlers/monadic syntax) for recursive calls.
    2. Wrap the final result of your computation in Trampoline.return.
    3. Execute the computation using Trampoline.run.
    (* Standard recursive function that might stack overflow in JSOO *)
    let some_function x =
      let rec f x =
        let a = f (x - 1) in
        let b = f (x - 2) in
        some_reduce a b
      in
      f x
    
    (* Re-written version using Trampoline to prevent stack overflow *)
    let some_function_safe x =
      let rec f x =
        let%bind.Trampoline a = f (x - 1) in
        let%bind.Trampoline b = f (x - 2) in
        Trampoline.return (some_reduce a b)
      in
      Trampoline.run (f x)
  4. Use match%sub [%lazy] to optimize large match branches

    master

    By default, Bonsai's switch function traverses all branches in a match%sub when constructing the graph. For large or complex graphs, this can be expensive. To prevent unnecessary computation of branches that might never be used, use the [%lazy] modifier.

    To use [%lazy], you must have a graph variable in scope. If your graph variable is not named graph, you must specify its name inside the [%lazy ...] block.

    Example with default graph name:

    let f (either_value : (_, _) Either.t Value.t) page1 page2 graph =
      let open Bonsai.Let_syntax in
      match%sub [%lazy] either_value with
      | First (a, b) -> page1 a b
      | Second x -> page2 x

    Example with custom graph name my_graph_7:

    let f (either_value : (_, _) Either.t Value.t) page1 page2 my_graph_7 =
      let open Bonsai.Let_syntax in
      match%sub [%lazy my_graph_7] either_value with
      | First (a, b) -> page1 a b
      | Second x -> page2 x
  5. Write automated UI tests with Bonsai

    master

    Bonsai allows you to write realistic, programmatic tests that manipulate UI elements and assert changes in the DOM. You can use Handle to interact with components and expect blocks to assert the state of the UI, including diffs showing how HTML attributes or class names change.

    Example of testing a text input that updates a message:

    let%expect_test "shows hello to a specified user" =
      let handle = Handle.create (Result_spec.vdom Fn.id) hello_textbox in
      Handle.show handle;
      [%expect
        {|
        <div>
          <input oninput> </input>
          <span> hello  </span>
        </div> |}];
      Handle.input_text handle ~get_vdom:Fn.id ~selector:"input" ~text:"Bob";
      Handle.show_diff handle;
      [%expect
        {|
          <div>
            <input oninput> </input>
    -      <span> hello  </span>
    +      <span> hello Bob </span>
          </div> |}];
  6. Use let%arr to enforce computation sharing

    master

    In Bonsai, a common mistake is to use let%map in a way that creates a new computation every time a value is used, leading to duplicated work. let%arr is designed to prevent this by forcing the user to save computations as Computation.t handles.

    let%arr uses the arr function from Let_syntax, which lifts a function to an arrow. The signature of arr is:

    val arr : here:[%call_pos] -> 'a Value.t -> f:('a -> 'b) -> 'b Computation.t

    Because arr returns a Computation.t, the only way to access the underlying Value.t is via let%sub. This pattern encourages developers to use as much sharing as possible by binding the result of a computation to a variable once, rather than re-running the mapping logic multiple times.

  7. Use match%sub to match on Value.t

    master

    The match%sub syntax allows you to pattern match on a Value.t (rather than a Computation.t) by relying on a switch function. This is useful when you want to branch your logic based on the structure of a value while maintaining the ability to bind components of that value to variables in the resulting branches.

    Note that match%sub requires the expression being matched to be of type Value.t.

    Example usage:

    let f (either_value : (_, _) Either.t Value.t) page1 page2 =
      let open Bonsai.Let_syntax in
      match%sub either_value with
      | First (a, b) -> page1 a b
      | Second x -> page2 x
    ";
    let f (either_value : (_, _) Either.t Value.t) page1 page2 =
      let open Bonsai.Let_syntax in
      match%sub either_value with
      | First (a, b) -> page1 a b
      | Second x -> page2 x