-
Notifications
You must be signed in to change notification settings - Fork 44
Description
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.tThat 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
endBut 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
endMy 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.tWhich 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