From 17667c274aef755c177d7ec9dc0c473651e25231 Mon Sep 17 00:00:00 2001 From: gered Date: Wed, 12 Jan 2022 17:18:04 -0500 Subject: [PATCH] update dependencies and fix things in views.reagent(+ .sente) --- README.md | 46 +-- views.reagent.sente/project.clj | 18 +- .../src/views/reagent/sente/client.cljs | 21 +- views.reagent/README.md | 261 ++++++++---------- views.reagent/project.clj | 14 +- .../src/views/reagent/client/component.clj | 11 +- 6 files changed, 172 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index 813ee10..4db02fc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # views.reagent -[Reagent][1] plugin for the [views][2] library, providing real-time component updates to server-side changes to data. +[Reagent][1] plugin for the [views][2] library, providing real-time component updates to +server-side changes to data. [1]: https://github.com/reagent-project/reagent [2]: https://github.com/gered/views @@ -11,40 +12,48 @@ This library is made up of two core parts: * The actual library, views.reagent, providing core functionality. -* A client/server communications plugin library which provides the glue code between whatever underlying client/server library you're using (e.g. [Sente][3] or [clj-browserchannel][4]) and views.reagent. +* A client/server communications plugin library which provides the glue code between whatever + underlying client/server library you're using (most likely [Sente][3]) and views.reagent. [3]: https://github.com/ptaoussanis/sente -[4]: https://github.com/gered/clj-browserchannel -To use views.reagent in your application, you need to add both the main library and one client/server communications plugin library as dependencies. See their respective pages linked to below for more information on doing this. +To use views.reagent in your application, you need to add both the main library and one +client/server communications plugin library as dependencies. See their respective pages linked +to below for more information on doing this. ### Main Library Documentation -[See here for full documentation.][5] +[See here for full documentation.][4] -[5]: https://github.com/gered/views.reagent/tree/master/views.reagent +[4]: https://github.com/gered/views.reagent/tree/master/views.reagent ### Client/Server Plugin Documentation -* **[views.reagent.sente][6]** provides fairly low-level integration with Sente. -* **[views.reagent.browserchannel][7]** for using BrowserChannel for client/server communication. +* **[views.reagent.sente][5]** provides fairly low-level integration with Sente. -[6]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente -[7]: https://github.com/gered/views.reagent/tree/master/views.reagent.browserchannel +[5]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente + +If you're intent on using something else, you'll need to write your own client/server plugin +library. Previously I provided a BrowserChannel plugin in addition to the Sente plugin library, +but BrowserChannel is now pretty ancient and unnecessary since modern browsers universally support +Websockets, so it was removed in favour of using Sente. ### Examples -There are two example applications for you to look at to see a fully working web application with working views system configured and working. +There are two example applications for you to look at to see a fully working web application with +working views system configured and working. -* Todo MVC. There are two versions of this that are both largely identical except that [one uses Sente][8] and the [other uses BrowserChannel][9]. -* [Class Registry][10]. This is a somewhat more complex application with a busy UI showing a bunch of data at once, but it does serve to show how a UI can be built from multiple different views at once. This example app uses Sente. +* [Todo MVC][6]. This is a copy of the original Reagent "Todo MVC" example app, but re-worked to + use a SQL database and the views system. +* [Class Registry][7]. This is a somewhat more complex application with a busy UI showing a bunch + of data at once, but it does serve to show how a UI can be built from multiple different views + at once. -[8]: https://github.com/gered/views.reagent/tree/master/examples/todomvc -[9]: https://github.com/gered/views.reagent/tree/master/examples/todomvc-browserchannel -[10]: https://github.com/gered/views.reagent/tree/master/examples/class-registry +[6]: https://github.com/gered/views.reagent/tree/master/examples/todomvc +[7]: https://github.com/gered/views.reagent/tree/master/examples/class-registry ### Notes @@ -55,15 +64,12 @@ like to integrate client/server communications in their applications. I wanted t (as much as possible) doing anything that would require any specific way of doing this kind of integration. -As well, speaking for myself, I use my own custom helper library that wraps over Sente which -I like but did not want to force anyone else to use. - The client/server glue code provided by these libraries is incredibly light so if they do not meet your needs for whatever reason you should find it easy to create one yourself. ## License -Copyright © 2016 Gered King +Copyright © 2022 Gered King Distributed under the the MIT License. See LICENSE for more details. diff --git a/views.reagent.sente/project.clj b/views.reagent.sente/project.clj index 5014268..d9f4bfa 100644 --- a/views.reagent.sente/project.clj +++ b/views.reagent.sente/project.clj @@ -1,21 +1,21 @@ -(defproject gered/views.reagent.sente "0.2-SNAPSHOT" +(defproject net.gered/views.reagent.sente "0.2-SNAPSHOT" :description "Sente client/server messaging adapter for views.reagent." :url "https://github.com/gered/views.reagent" :license {:name "MIT License" :url "http://opensource.org/licenses/MIT"} - :dependencies [[org.clojure/clojure "1.8.0"]] + :dependencies [] - :plugins [[lein-cljsbuild "1.1.3"]] + :plugins [[lein-cljsbuild "1.1.8"]] :profiles {:provided {:dependencies - [[org.clojure/clojure "1.8.0"] - [org.clojure/clojurescript "1.8.51"] - [reagent "0.6.0-alpha"] - [gered/views "1.5"] - [gered/views.reagent "0.1"] - [com.taoensso/sente "1.8.1"]]}} + [[org.clojure/clojure "1.10.3"] + [org.clojure/clojurescript "1.10.773"] + [reagent "1.1.0"] + [net.gered/views "1.6-SNAPSHOT"] + [net.gered/views.reagent "0.2-SNAPSHOT"] + [com.taoensso/sente "1.16.2"]]}} :cljsbuild {:builds {:main diff --git a/views.reagent.sente/src/views/reagent/sente/client.cljs b/views.reagent.sente/src/views/reagent/sente/client.cljs index f11905e..1d312f3 100644 --- a/views.reagent.sente/src/views/reagent/sente/client.cljs +++ b/views.reagent.sente/src/views/reagent/sente/client.cljs @@ -21,6 +21,15 @@ (send-fn sente-chsk-map data)) (reset! send-buffer [])) +(defn chsk-open-event? + "returns true if the sente event is for a channel-socket state change to 'open' or 'connected'." + [{:keys [event] :as ev}] + ; for :chsk/state events, sente sends the event data in the form [old-state new-state]. + ; we only care about the new state for the purposes of performing this check ... + (let [[ev-id ev-data] event] + (and (= :chsk/state ev-id) + (:open? (second ev-data))))) + (defn on-open! "should be called when a new Sente connection is established. ev is the event map provided by Sente where id = :chsk/state, and :open? = true. make sure @@ -44,14 +53,12 @@ application does not need to do any custom Sente event handling, then you can opt to use this event handler." [sente-chsk-map {:keys [event id client-id] :as ev}] - (let [[ev-id ev-data] event] - (cond - (and (= :chsk/state ev-id) - (:open? ev-data)) - (on-open! sente-chsk-map ev) + (cond + (chsk-open-event? event) + (on-open! sente-chsk-map ev) - (= :chsk/recv id) - (on-receive! sente-chsk-map ev)))) + (= :chsk/recv id) + (on-receive! sente-chsk-map ev))) (defn init! "performs initial configuration necessary to hook Sente into views.reagent as the diff --git a/views.reagent/README.md b/views.reagent/README.md index 69d2654..357e158 100644 --- a/views.reagent/README.md +++ b/views.reagent/README.md @@ -1,11 +1,10 @@ # views.reagent -This is the main library for the [views.reagent project][1] which -provides the core functionality that most of your application code will -make use of. +This is the main library for the [views.reagent project][1] which provides the core functionality +that most of your application code will make use of. -Familiarity with the [views][2] library is *absolutely crucial* to -understanding and usage of views.reagent. +Familiarity with the [views][2] library is *absolutely crucial* to understanding and usage of +views.reagent. [1]: https://github.com/gered/views.reagent [2]: https://github.com/gered/views @@ -18,22 +17,19 @@ understanding and usage of views.reagent. ## Usage -Much of this documentation will be referring to the -[Todo MVC example project][3] which uses +Much of this documentation will be referring to the [Todo MVC example project][3] which uses [Sente client/server messaging][4] and [SQL views][5]. [3]: https://github.com/gered/views.reagent/tree/master/examples/todomvc [4]: https://github.com/gered/views.reagent/tree/master/views.reagent.sente [5]: https://github.com/gered/views.sql -Usage of this library is incredibly simple once you have a working -views system up and running. +Usage of this library is incredibly simple once you have a working views system up and running. -Initialization of the views system is typically either done directly -via `views.core/init!` with some special configuration for whatever -client/server messaging plugin you're using, or you may be required to -call a special "init" function provided by the plugin library to use -instead of `views.core/init!`. +Initialization of the views system is typically either done directly via `views.core/init!` with +some special configuration for whatever client/server messaging plugin you're using, or you may be +required to call a special "init" function provided by the plugin library to use instead of +`views.core/init!`. The Todo MVC example uses the following (server-side) view system: @@ -49,31 +45,26 @@ The Todo MVC example uses the following (server-side) view system: [(view :todos db #'todos-list)]) ``` -A single view named `:todos` which simply returns a list of all Todos -in the database. The view takes no parameters. +A single view named `:todos` which simply returns a list of all Todos in the database. The view +takes no parameters. -Over on the ClojureScript side of things, once a connection has been -established to the server by the client/server messaging library, you -are ready to start using **view cursors** in your Reagent components. +Over on the ClojureScript side of things, once a connection has been established to the server by +the client/server messaging library, you are ready to start using **view cursors** in your Reagent +components. ### View Cursors -A **view cursor** is simply a Reagent cursor that represents the -underlying view data received from the views library when the view is -subscribed to. We can create a subscription by simply creating a view -cursor for the desired view and giving it any appropriate parameters. -views.reagent will automatically determine if it's the first usage of -the view cursor and if a subscription request needs to be sent to the -server. When a view refresh is performed on the server for the view, -views.reagent sends it to the client and the data is put into a -location where it's available to the views cursor. Since view cursors -are Reagent cursors, updating the data like this instantly causes -components dereferencing the cursor to rerender themselves. When -components are unmounted views.reagent will automatically unsubscribe -from views as appropriate. As well, when parameters passed in to view -cursors change, view re-subscriptions are automatically handled to make -sure the view cursor is always up to date with the current client -state. +A **view cursor** is simply a Reagent cursor that represents the underlying view data received +from the views library when the view is subscribed to. We can create a subscription by simply +creating a view cursor for the desired view and giving it any appropriate parameters. +views.reagent will automatically determine if it's the first usage of the view cursor and if a +subscription request needs to be sent to the server. When a view refresh is performed on the +server for the view, views.reagent sends it to the client and the data is put into a location +where it's available to the views cursor. Since view cursors are Reagent cursors, updating the +data like this instantly causes components dereferencing the cursor to rerender themselves. When +components are unmounted views.reagent will automatically unsubscribe from views as appropriate. +As well, when parameters passed in to view cursors change, view re-subscriptions are automatically +handled to make sure the view cursor is always up to date with the current client state. So, how do we create a view cursor? @@ -91,36 +82,29 @@ So, how do we create a view cursor? @(view-cursor :todos))]) ``` -Given the previously set up view system on the server, this is all the -UI code necessary to subscribe to the `:todos` view and retrieve and -render the Todos list on the client. Whenever the `:todos` view is -refreshed on the server, the client will receive the data automatically -and the component will rerender since it is dereferencing the views -cursor. Finally, the client will automatically unsubscribe from the -`:todos` view when the `my-todos-list` component is unmounted. +Given the previously set up view system on the server, this is all the UI code necessary to +subscribe to the `:todos` view and retrieve and render the Todos list on the client. Whenever the +`:todos` view is refreshed on the server, the client will receive the data automatically and the +component will rerender since it is dereferencing the views cursor. Finally, the client will +automatically unsubscribe from the `:todos` view when the `my-todos-list` component is unmounted. -At first glance, `my-todos-list` looks just like any other normal -Reagent component. However a very important difference is the use of -`defvc` instead of `defn`. +At first glance, `my-todos-list` looks just like any other normal Reagent component. However a +very important difference is the use of `defvc` instead of `defn`. -`defvc` creates a Reagent **view component** which hooks into some -React component lifecycle events to automatically handle view -subscriptions/unsubscriptions for us based on how we use `view-cursor` -inside the component. You ***must*** use `defvc` for all Reagent -components within which you want to use `view-cursor`. +`defvc` creates a Reagent **view component** which hooks into some React component lifecycle +events to automatically handle view subscriptions/unsubscriptions for us based on how we use +`view-cursor` inside the component. You ***must*** use `defvc` for all Reagent components within +which you want to use `view-cursor`. -`view-cursor` returns a Reagent cursor containing the actual view data -(in this case, a simple list of Todos). It's important to note that at -first the view data returned will be `nil` since obviously the client -must first wait for the subscription to be processed by the server and -then for the server to send back the initial view data. Once this -happens the component will automatically rerender as you would expect -(showing the list of todos). +`view-cursor` returns a Reagent cursor containing the actual view data (in this case, a simple +list of Todos). It's important to note that at first the view data returned will be `nil` since +obviously the client must first wait for the subscription to be processed by the server and then +for the server to send back the initial view data. Once this happens the component will +automatically rerender as you would expect (showing the list of todos). -You can check if the view cursor is still waiting on the initial set of -data through the use of the `loading?` function. This can be used to -render some kind of "loading" message or something similar if you'd -prefer not to render empty data when components first load. +You can check if the view cursor is still waiting on the initial set of data through the use of +the `loading?` function. This can be used to render some kind of "loading" message or something +similar if you'd prefer not to render empty data when components first load. ```clj (defvc my-todos-list [] @@ -135,20 +119,18 @@ prefer not to render empty data when components first load. @todos)]))) ``` -Note that `loading?` should be passed the actual Reagent cursor that -`view-cursor` returns, not the dereferenced result. +Note that `loading?` should be passed the actual Reagent cursor that `view-cursor` returns, not +the dereferenced result. -Also remember that `loading?` only checks if the view cursor is waiting -on the **initial** view data. Once that first set of data is received, -`loading?` will always return false. There is no current method in -views.reagent for determining if a view refresh is pending, although -this is typically somewhat of a less drastic UI change to the user so -in practice it may be less of a concern. +Also remember that `loading?` only checks if the view cursor is waiting on the **initial** view +data. Once that first set of data is received, `loading?` will always return false. There is no +current method in views.reagent for determining if a view refresh is pending, although this is +typically somewhat of a less drastic UI change to the user so in practice it may be less of a +concern. #### View Parameters -Some of your views may take parameters. This is easily supported by -views.reagent. +Some of your views may take parameters. This is easily supported by views.reagent. As an example, if our `:todos` view was updated to include a filter: @@ -182,26 +164,23 @@ Letting us do any of these on the client: #### View Cursors Are Intended To Be Read-only -Even though a Reagent cursor allows you to update them as well as read -from them, updating a view cursor doesn't do anything. Nothing stops -you from doing this, but updating a view cursor does not propagate -changes to the server or anything like that. In addition, you will lose -any changes you make every time a view refresh is received as the data -gets blindly replaced. +Even though a Reagent cursor allows you to update them as well as read from them, updating a view +cursor doesn't do anything. Nothing stops you from doing this, but updating a view cursor does not +propagate changes to the server or anything like that. In addition, you will lose any changes you +make every time a view refresh is received as the data gets blindly replaced. It is recommended that you do not write code that updates a view cursor. ## Advanced Topics -Most people just looking to use views.reagent in their applications -probably won't need to read anything in this section. +Most people just looking to use views.reagent in their applications probably won't need to read +anything in this section. ### Manually Managing View Subscriptions -For those applications with very specific/complex requirements, you can -manually subscribe and unsubscribe to views from your ClojureScript -code using views.reagent as well as make use of view cursors outside of -Reagent components. I do not recommend this though. +For those applications with very specific/complex requirements, you can manually subscribe and +unsubscribe to views from your ClojureScript code using views.reagent as well as make use of view +cursors outside of Reagent components. I do not recommend this though. ```clj (use 'views.reagent.client.core) @@ -217,50 +196,41 @@ Reagent components. I do not recommend this though. (unsubscribe! [{:view-id :todos :parameters []}]) ``` -When using these low-level functions, you need to specify view -signature maps (a.k.a. "view sigs") to refer to the views you want to -use. Also note that unlike the server-side view signatures that you may -be familiar with from the views library, these client-side view -signatures never have a `:namespace` in them. Even if you include one, -it is disregarded by the server. +When using these low-level functions, you need to specify view signature maps (a.k.a. "view sigs") +to refer to the views you want to use. Also note that unlike the server-side view signatures that +you may be familiar with from the views library, these client-side view signatures never have a +`:namespace` in them. Even if you include one, it is disregarded by the server. -Also note that `subscribe!` and `unsubscribe!` both take a list of view -signatures, so you can subscribe and unsubscribe from multiple views at -once. +Also note that `subscribe!` and `unsubscribe!` both take a list of view signatures, so you can +subscribe and unsubscribe from multiple views at once. -I do not recommend mixing use of these low-level functions and using -`defvc` components. You should probably pick one or the other and stick -to it unless you really know what you're doing. +I do not recommend mixing use of these low-level functions and using `defvc` components. You +should probably pick one or the other and stick to it unless you really know what you're doing. ### Integration Points for Writing a Client/Server Messaging Plugin -If you would like to write your own client/server messaging plugin -library to fit your own needs you can easily do so. There are a couple -integration points within the main views.reagent library, as well as -some special configuration you will need to provide to the views system -that you need to be aware of. +If you would like to write your own client/server messaging plugin library to fit your own needs +you can easily do so. There are a couple integration points within the main views.reagent library, +as well as some special configuration you will need to provide to the views system that you need +to be aware of. #### View Subscriber Key -In the views library, a bunch of functions take a "subscriber key." -This is an arbitrary value that uniquely identifies a subscriber. There -can of course be multiple views for each subscriber key. Said another -way: someone (uniquely identified by the subscriber key) can be -subscribed to multiple different views at the same time. +In the views library, a bunch of functions take a "subscriber key." This is an arbitrary value +that uniquely identifies a subscriber. There can of course be multiple views for each subscriber +key. Said another way: someone (uniquely identified by the subscriber key) can be subscribed to +multiple different views at the same time. -You will typically want to use the underlying client/server library's -"client/user connection ID" as the subscriber key. For Sente this is -the "user id" or the "client id" (depending on your application), and -for clj-browserchannel this is the BrowserChannel "session id." +You will typically want to use the underlying client/server library's "client/user connection ID" +as the subscriber key. For Sente this is almost certainly going to be the "user id" or the +"client id" (depending on your application). #### View System Configuration -You need to provide a `:send-fn` function that can be provided in the -options given to `views.core/init!`. This function is used by the views -library to send view refreshes to subscribers. For views.reagent, your -send-fn function should send a vector with 3 things in it: the keyword -`:views/refresh` followed by the `view-sig` and then `view-data`. For -example: +You need to provide a `:send-fn` function that can be provided in the options given to +`views.core/init!`. This function is used by the views library to send view refreshes to +subscribers. For views.reagent, your send-fn function should send a vector with 3 things in it: +the keyword `:views/refresh` followed by the `view-sig` and then `view-data`. For example: ```clj (defn send-fn @@ -270,50 +240,43 @@ example: #### Server-side Handling -`views.reagent.server.core` has two main event handling functions that -provide proper handling for client connection events: +`views.reagent.server.core` has two main event handling functions that provide proper handling +for client connection events: -* `on-close!` should be called when a client's connection is closed for -whatever reason. views.reagent will remove all of the client's -subscriptions. -* `on-receive!` should be called and passed in the raw data from every -message received from the client. It will return `true` if -views.reagent recognized and handled the message as a subscription or -unsubscription request. See `views.reagent.utils/relevant-event?` for -how it recognizes relevant messages. +* `on-close!` should be called when a client's connection is closed for whatever reason. + views.reagent will remove all of the client's subscriptions. +* `on-receive!` should be called and passed in the raw data from every message received from the + client. It will return `true` if views.reagent recognized and handled the message as a + subscription or unsubscription request. See `views.reagent.utils/relevant-event?` for how it + recognizes relevant messages. -For both of these functions, `client-id` should be the subscriber key -from the views library, and `context` can be anything you wish. -Typically you will want to use something like a Ring request map and/or -user profile data as the context. This context argument is what gets -passed to `views.core/subscribe!` and `views.core/unsubscribe!`. +For both of these functions, `client-id` should be the subscriber key from the views library, and +`context` can be anything you wish. Typically you will want to use something like a Ring request +map and/or user profile data as the context. This context argument is what gets passed to +`views.core/subscribe!` and `views.core/unsubscribe!`. #### Client-side Handling -`views.reagent.client.core` also has two main event handling functions -that provide proper handling for server connection events: +`views.reagent.client.core` also has two main event handling functions that provide proper +handling for server connection events: -* `on-open!` should be called when a connection to the server is -established (and re-established, if applicable). views.reagent will -re-send subscription requests for any subscriptions that should exist -(e.g. if the connection was lost and the application reconnected, -resubscribe to the views that all current mounted components need). -* `on-receive!` should be called and passed in the raw data from every -message received from the server. It will return `true` if -views.reagent recognized and handled the message as a view refresh -event. See `views.reagent.utils/relevant-event?` for how it recognizes -relevant messages. +* `on-open!` should be called when a connection to the server is established (and re-established, + if applicable). views.reagent will re-send subscription requests for any subscriptions that + should exist (e.g. if the connection was lost and the application reconnected, resubscribe to + the views that all current mounted components need). +* `on-receive!` should be called and passed in the raw data from every message received from the + server. It will return `true` if views.reagent recognized and handled the message as a view + refresh event. See `views.reagent.utils/relevant-event?` for how it recognizes relevant messages. -You also need to provide a function to send messages to the server. -You can set this function by directly using `reset!` on the atom -`views.reagent.client/send-fn`. The function should take a single -argument which is the data to be sent. +You also need to provide a function to send messages to the server. You can set this function by +directly using `reset!` on the atom `views.reagent.client/send-fn`. The function should take a +single argument which is the data to be sent. -You should take care to hook up all of these integration points before -the first Reagent component is rendered at page load. +You should take care to hook up all of these integration points before the first Reagent component +is rendered at page load. ## License -Copyright © 2016 Gered King +Copyright © 2022 Gered King Distributed under the the MIT License. See LICENSE for more details. diff --git a/views.reagent/project.clj b/views.reagent/project.clj index 3c9af33..6dc881b 100644 --- a/views.reagent/project.clj +++ b/views.reagent/project.clj @@ -1,19 +1,19 @@ -(defproject gered/views.reagent "0.2-SNAPSHOT" +(defproject net.gered/views.reagent "0.2-SNAPSHOT" :description "Reagent plugin for the views library, providing real-time component updates to server-side changes to data." :url "https://github.com/gered/views.reagent" :license {:name "MIT License" :url "http://opensource.org/licenses/MIT"} - :dependencies [[org.clojure/tools.logging "0.3.1"]] + :dependencies [[org.clojure/tools.logging "1.2.4"]] - :plugins [[lein-cljsbuild "1.1.3"]] + :plugins [[lein-cljsbuild "1.1.8"]] :profiles {:provided {:dependencies - [[org.clojure/clojure "1.8.0"] - [org.clojure/clojurescript "1.8.51"] - [reagent "0.6.0-alpha"] - [gered/views "1.5"]]}} + [[org.clojure/clojure "1.10.3"] + [org.clojure/clojurescript "1.10.773"] + [reagent "1.1.0"] + [net.gered/views "1.6-SNAPSHOT"]]}} :cljsbuild {:builds {:main diff --git a/views.reagent/src/views/reagent/client/component.clj b/views.reagent/src/views/reagent/client/component.clj index c801eb4..f6c6662 100644 --- a/views.reagent/src/views/reagent/client/component.clj +++ b/views.reagent/src/views/reagent/client/component.clj @@ -28,8 +28,8 @@ [args & body] decl] `(defn ~component-name ~attr-map [] (reagent.core/create-class - {:component-will-mount - (fn [this#] + {:constructor + (fn [this# props#] (views.reagent.client.component/prepare-for-render! this#)) :component-did-mount @@ -43,14 +43,11 @@ (fn [this#] (views.reagent.client.component/unsubscribe-all! this#)) - :component-will-receive-props - (fn [this# new-argv#] - (views.reagent.client.component/prepare-for-render! this#)) - :component-did-update (fn [this# old-argv#] (views.reagent.client.component/update-subscriptions! this#)) - :component-function + :reagent-render (fn ~args + (views.reagent.client.component/prepare-for-render! (reagent.core/current-component)) ~@body)}))))