Throw away manual server state management on the client and get realtime collaboration for free.
You have your backend with REST API. You have your React-based frontend. Usually you use ajax calls and keep out-of-date independent incomplete copy of your server state on the client. When you make ajax calls, you update your local copy of the state. With live-components you throw away manual handling of server state on the client. You have one component that takes a list of subscription urls, and a component, which will receive data from those urls and do the rendering. On the server whenever you think some urls may have new data, you mark them as updated and new data is automatically pushed to all connected clients. Magic! 🦄🦄🦄
Clone the repo, cd into examples/todomvc
and run boot dev
. After that you may open http://localhost:3000
in two windows and see how adding/changing todos in one window adds/changes them in both.
(at the bottom there are links to 3 commits that augment reagent's todomvc with live-components)
-
Add library to your project:
[live-components "1.0.1"]
or see https://clojars.org/live-components -
Wrap your ring handler with live:
(ns app.server
(:require [live-components.server.core :as live-server]))
(live-server/wrap-live app "/live")
"/live"
is a url under which live endpoint will become available. You'll use it on the client to connect to live (see below).
You have to use immutant web server, as live server middleware assumes immutant's websocket api. Unfortunately ring doesn't have standard websocket api. See examples/todomvc/src/app/server.clj for example of starting immutant.
- Configure and enable live on the client:
(ns app.client
(:require [live-components.client.core :as live-client]))
(defn transform-response [[uri ring-response]]
[uri (update ring-response
:body #(-> (js/JSON.parse %)
(js->clj {:keywordize-keys true})))])
(live-client/enable! (str (clojure.string/replace (.-protocol js/window.location) "http" "ws")
"//"
js/document.location.host
"/live")
transform-response)
transform-response
defines how your data is deserialized. If you'll use identity
, you'll get your api response body left as a string. If your api returns json, you most likely want to use js/JSON.parse
and js->clj
as in the example above. If you use EDN, use cljs.reader/read-string
.
- Use live components:
(ns app.client.posts-page
(:require [reagent.core :as r]
[live-components.client.components :as lc]))
(defn post [{:keys [title body]}]
[:div.post
[:h2 title]
[:div.post-text body]])
(defn loading-component []
[:div])
(defn unexpected-component [response]
[:div.error (pr-str response)])
(defn live-post [post-id]
[lc/live-component [(str "/posts/" id)] post loading-component unexpected-component])
(defn root-component []
[:div [:h1 "My Blog"]
[live-post 1]])
(r/render [root-component] (js/document.getElementById "app"))
loading-component
takes no arguments and will be rendered until data is received.
unexpected-component
takes one argument which is a vector of responses. It is rendered if any api call responded with any http status other than 200.
- Tell us when your data have been updated:
(ns app.server.posts
(:require [live-components.server.core :as live]))
(defn update-post [post-id {:keys [title body]}]
(when title (exec-sql "UPDATE posts SET title = % WHERE id = %" title post-id))
(when body (exec-sql "UPDATE posts SET body = % WHERE id = %" body post-id))
(when (or title body)
(live/mark-updated (str "/posts/" post-id))
(live/mark-updated "/posts")))
- DONE
First argument to [lc/live-component]
is a vector of urls, and each result will be passed as an argument to your rendering component, so you are not limited to just one url per component. That means you can have
(defn post [post user] ...)
and use it like [lc/live-component ["/posts/1" "/users/1"] post loading-component unexpected-component]
Most likely you want to write a thin wrapper around lc/live-component
to intelligently generate urls, and to use same loading and unexpected components across your project. See examples/counter as an example with bidi routing and wrappers.
See examples/todomvc for reagent's todomvc augmented with live-components. It is done in following 3 commits:
- dd994a1 add todo rest api. At this point you have simple rest api for managing todos that you can call using
curl
. - b9fad3b todo live rendering. At this point your server todos are rendered live in a browser. Whenever you make rest api call (e.g. via curl) to modify todos, the changes are automatically propagated to all clients.
- 72093db todo live modification. At this point we have fully-collaborative todomvc.
This project have been born as part of a continuous effort to build the best social file sharing platform http://ourmedian.com.