-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to best design higher-order components #20
Comments
Here's a function I just wrote that should be helpful: let component
: type a view.
(module Bonsai.Model with type t = a)
-> a Effect.t Value.t
-> view_while_loading:view Value.t
-> (a Value.t -> view Computation.t)
-> view Computation.t
=
fun (module Model) effect ~view_while_loading computation ->
let%sub data, set_data = Bonsai.state_opt (module Model) in
let%sub on_activate =
let%arr effect = effect
and set_data = set_data in
let%bind.Effect data_fetched_from_server = effect in
set_data (Some data_fetched_from_server)
in
let%sub () = Bonsai.Edge.lifecycle ~on_activate () in
match%sub data with
| None -> return view_while_loading
| Some data -> computation data
;; Hopefully the type signature makes the purpose of this function clear enough. There are a lot of parts to this function, and I will surely gloss over an important detail, so feel free to ask for a clarification on any of it. Here are some points of interest:
val sub : 'a Computation.t -> f:('a Value.t -> 'b Computation.t) -> 'b Computation.t If you're used to monads,
Note that I'm happy to elaborate further if needed. |
Thank you very much for the example and detailed explanation! After reading over this and the mli again, I think Bonsai may have finally clicked for me. I was successfully able to adapt this to my use case (and define a I think the only major piece I'm unsure of is
Is |
I think it makes sense to answer your second question first. Your question inspired me to write some documentation that is not yet on github, so I'll quote it here. It's not a direct answer, but you may find it useful. When you work with Bonsai, it can be a bit tricky to know when different parts of your code run. Here is a list of several different "*-time"s that you should be aware of.
The above three categories are the typical things you have to think about with any OCaml program. Bonsai further subdivides runtime:
In summary, below is some code with print statements representing the various times I just mentioned. You may notice that we aren't printing anything at eval-time; this is because evaluation doesn't execute any user code and is therefore difficult to witness. open! Core
open! Bonsai_web
let component () =
let%sub state, set_state = Bonsai.state (module Bool) ~default_model:true in
print_endline "construction-time";
let%sub view =
let%arr state = state in
print_endline "stabilize-time";
Vdom.Node.text (Bool.to_string state)
in
let%sub on_click_effect =
let%arr set_state = set_state in
print_endline "stabilize-time; here we're merely computing the effect, not running it yet";
let%bind.Effect () =
Effect.of_sync_fun print_endline "frame-boundary-time; this is when the effect actually runs"
in
set_state false
in
let%arr view = view
and on_click_effect = on_click_effect in
Vdom.Node.div
~attr:(Vdom.Attr.on_click (fun _ -> on_click_effect))
[ view ]
;;
let (_ : _ Start.Handle.t) =
let app = component () in
print_endline "finished construction-time; starting eval-time";
Start.start Start.Result_spec.just_the_view ~bind_to_element_with_id:"app" app
;; Regarding Dynamic scope is a programming language concept that concerns when you can refer to variables. Most languages default to lexical because it is much easier to think about; lexical scope forces you to explicitly state your inputs, instead of relying implicitly on the environment of the caller. The same trade-off applies with the |
Sorry for the late reply, ever since finishing my class project I've been swamped with finals and haven't had the chance to revisit Bonsai in depth. Thank you so much for your explanations; this has helped a ton in better understanding Bonsai, its goals, and its design philosophy.
Gotcha, that and the design vs compile vs runtime explanation make sense.
I remember covering the concept in my intro to PL class last year, but I think I'm still struggling a bit to understand how it would be used since I haven't seen "real life" examples of it in Bonsai. I might be totally off here, but is the intent similar to React's Context feature, adding support for shared "semi-global" state among a subgraph of computation, as well as being able to inject context in one place without having it be explicit input for all computations in the subgraph? If so:
There's also another pattern I'm trying to figure out, but I'll open a new issue for that as to not deviate from the original topic even further. Thank you again so much for your help with this! |
Haha, yeah; there aren't a ton of usages of the API in total if I'm honest haha! Our main motivation was to be able to parameterize large swaths of the component tree with things like styling or theming directives. "is dark mode" would be a good example of something that could be put into a dynamic-var and then read from many components without those components needing to have the value passed into them directly. Not the best example because "is-dark-mode" is likely going to be global to the whole component tree, but technically you could set bits and pieces of it as "dark mode" and others as "light mode" with this API.
Yep! Context is react's implementation of dynamically-scoped variables! Emacs-lisp also (infamously) has dynamically scoped variables by default!
Correct!
Yes!
The
Ah, that documentation is out of date, I'll fix that! Either way, if this description is accurate, why not separate getting/setting values? This test case, for example, seems like it would lead to anti-patterns. I'm not sure what you mean by this; setting and getting them are separated into the
Precisely. This is pretty useful when developing higher-order components, where you may want to override a value for one part of your component, but then revert back to the previous value when evaluating the first-class-component that your user passed in.
I think I cleared this up above, but just to cover everything: the
I'm always looking for ways to improve the docs, so thanks for your feedback! In the meantime, you might be well-served by reading the Racket documentation on "parameters", we basically just ripped off that whole API.
Not at all! Best of luck with your project! |
Ah that explains a lot, thank you! So would it be correct to say that:
Maybe this is just my unfamiliarity with dynamically scoped syntaxes, but to me, "set" implies a void function where subsequent Out of curiosity, had you considered an API similar to:
On a slight tangent, it feels like if using |
yep!
that’s pretty close to the implementation, but I think of it as being the same as ‘set’ but with an “undo” operation to revert it back to the previous state.
My initial implementation did this, but it was hard to reason about because a subcomponent of a subcomponent could ‘set’ and unintentionally override things in the rest of your program. It was basically like having global mutable variables. The current API is very explicit about the computation that can witness the call to “set”.
maybe I’m missing something, but isn’t this basically the api for ‘set’ right now?
hmm, I hadn’t thought about it, but it could be possible and would be pretty cool! |
Structurally it's the same, but since the primary "operation" done by |
Hi @TyOverby and @pmwhite! I wanted to thank you again for helping me understand Bonsai and answering my questions. While working on my project, I wrote a brief overview of Bonsai since writing explanations helps me work through complex concepts. After my finals ended and I had a bit more free time, I decided to expand it into a tutorial series on OCaml full-stack web development, based around my project. I really enjoyed learning Bonsai, using it in my project, and writing about it. The core idea is incredibly general and flexible, with a relatively lightweight API. I'm looking forward to seeing where it goes next! Thanks again for your help. |
@askvortsov1 That is so cool! Thanks for sharing! |
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
orBonsai.state_machine1
, yielding the type:That brings me to the query loader component. Ideally, I'd like something along the lines of:
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:
My current implementation (below) uses the
Bonsai.of_module1
pattern, but I've also tried doing this withBonsai.state_machine
, and haven't been able to find a combination of combinators that avoids the nestedComputation.t Computation.t
. One attempt that came close was having the inner component be of the form:Which avoided the nested
Computation.t
, but lost the ability to have theQ.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:
The text was updated successfully, but these errors were encountered: