Skip to content

How to best design higher-order components #20

@askvortsov1

Description

@askvortsov1

As part of a recent school project on database-driven web applications, I've decided to explore the OCaml web programming ecosystem, with Dream for the backend, Bonsai for the frontend, and a graphql layer in between. I'm pretty familiar with React/Mithril, so I'm comfortable with vdom-based frontends, but I'm having a bit of trouble grasping some Bonsai concepts.

To cut back on code duplication, I want to build a higher-order GraphQL Query Loader component, which would execute a graphql query, and when it gets a result, render a "child" component.

I've figured out that the inner component should probably be constructed with Bonsai.of_module1 or Bonsai.state_machine1, yielding the type:

val component: Q.t Value.t -> Vdom.Node.t Computation.t

That brings me to the query loader component. Ideally, I'd like something along the lines of:

module Loader (Q: Query) = struct
    val component: (Q.t Value.t -> Vdom.Node.t Computation.t) -> Vdom.Node.t Computation.t
end

But this is where I've reached a bit of a dead end. Regardless of what I've tried so far, I keep ending up with:

module Loader (Q: Query) = struct
    val component: (Q.t Value.t -> Vdom.Node.t Computation.t) -> Vdom.Node.t Computation.t Computation.t
end

My current implementation (below) uses the Bonsai.of_module1 pattern, but I've also tried doing this with Bonsai.state_machine, and haven't been able to find a combination of combinators that avoids the nested Computation.t Computation.t. One attempt that came close was having the inner component be of the form:

val component: (Q.t -> Vdom.Node.t) Computation.t

Which avoided the nested Computation.t, but lost the ability to have the Q.t input factored into the computation, so the inner component didn't redraw when the data came in.

Are there any cleaner design patterns I can use for higher-order components like this?


Current Query Loader Code:

open! Core
open! Bonsai_web
open Bonsai.Let_syntax

(* A collection of GraphQL modules generated by graphql-ppx *)
module G = Nittany_market_frontend_graphql

module Loader (Q : G.Queries.Query) = struct
  module T = struct
    module Input = struct
      type t = Q.t option Value.t -> Vdom.Node.t Computation.t
    end

    module Model = struct
      type t = { loaded : bool; res : Q.t option }

      (* The query result doesn't matter for diffing purposes so I excluded it *)
      let sexp_of_t t = Sexplib0.Sexp.Atom (Bool.to_string t.loaded)

      let t_of_sexp v =
        { loaded = Bool.of_string (Sexplib0.Sexp.to_string v); res = None }

      let equal a b = Bool.equal a.loaded b.loaded
    end

    module Action = struct
      module SexpableQ = G.Queries.SexpableQuery (Q)

      (* Although funnily enough, then it turned out it does need to be sexpable
         so that the action can be encoded, so I had to make a functor for that anyway :) *)
      type t = Loaded of SexpableQ.t option | Unloaded [@@deriving sexp_of]
    end

    module Result = Vdom.Node

    let apply_action ~inject:_ ~schedule_event:_ _input model
        (_action : Action.t) =
      model

    module Client = G.Client.ForQuery (Q)

    let compute ~inject (input : Input.t) (model : Model.t) =
      if model.loaded then
        (* Ideally I'd like to return Vdom.Node.t here,
        but there doesn't seem to be a clear way to do so. *)
        input (Value.return model.res)
      else
        (* A possibly hacky way to run the graphql query. *)
        let _ = 
        (Lwt.ignore_result
         (Lwt.map
            (fun (_resp, body) ->
              Vdom.Effect.Expert.handle_non_dom_event_exn
                (inject (Action.Loaded body)))
            (Client.query ()))) in
            Vdom.Node.text "Loading"

    let name = Source_code_position.to_string [%here]
  end

  module Action = struct end

  let component =
    Bonsai.of_module1 (module T) ~default_model:{ loaded = false; res = None }
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    forwarded-to-js-devsThis report has been forwarded to Jane Street's internal review system.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions