Server-side rendering for Om Next components
Note: as of Om #764, server-side rendering support landed in Om, which makes this repository deprecated.
Leiningen dependency information:
Maven dependency information:
<dependency>
<groupId>com.ladderlife</groupId>
<artifactId>cellophane</artifactId>
<version>0.3.5</version>
</dependency>
Note: requires Om 1.0.0-alpha32 or later.
Cellophane provides a Clojure implementation of Om Next, which is just enough to get components to render server-side into an HTML string that can be picked up by React on the browser.
If you want to take full advantage of server-side rendering, you need to:
- Port your component code to
.cljc
- Port your client parser code to
.cljc
- Require Cellophane in the
:clj
branches of your namespaces, as shown below. - Use a different
send
function in the Cellophane reconciler. Here's an example for projects using the Om Next parser server-side.
(ns my-project.core
(:require #?@(:cljs [[om.next :as om :refer-macros [defui]]
[om.dom :as dom]]
:clj [[cellophane.next :as om :refer [defui]]
[cellophane.dom :as dom]])))
Server-side rendering your components is very similar to what one would do in Om Next.
The simplest case is to feed dom/render-to-str
a component instance created with a factory. Here's an example:
(ns my-project.core
(:require [cellophane.next :as om :refer [defui]]
[cellophane.dom :as dom]))
(defui SimpleComponent
Object
(render [this]
(dom/div nil "Hello, world!")))
(def simple-factory (om/factory SimpleComponent))
(dom/render-to-str (simple-factory))
;; => "<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"1632637923\">Hello, world!</div>"
The full-blown case is shown below, and includes parser and reconciler code. The following code is directly taken from the Om Next Quick Start.
A full stack TodoMVC example with server-side rendering can be found in fullstack_example.
(def animals-app-state
(atom
{:app/title "Animals"
:animals/list
[[1 "Ant"] [2 "Antelope"] [3 "Bird"] [4 "Cat"] [5 "Dog"]
[6 "Lion"] [7 "Mouse"] [8 "Monkey"] [9 "Snake"] [10 "Zebra"]]}))
(defmulti animals-read (fn [env key params] key))
(defmethod animals-read :default
[{:keys [state] :as env} key params]
(let [st @state]
(if-let [[_ value] (find st key)]
{:value value}
{:value :not-found})))
(defmethod animals-read :animals/list
[{:keys [state] :as env} key {:keys [start end]}]
{:value (subvec (:animals/list @state) start end)})
(defui AnimalsList
static cellophane/IQueryParams
(params [this]
{:start 0 :end 10})
static cellophane/IQuery
(query [this]
'[:app/title (:animals/list {:start ?start :end ?end})])
Object
(render [this]
(let [{:keys [app/title animals/list]} (cellophane/props this)]
(dom/div nil
(dom/h2 nil title)
(apply dom/ul nil
(map
(fn [[i name]]
(dom/li nil (str i ". " name)))
list))))))
(def animals-reconciler
(cellophane/reconciler
{:state animals-app-state
:parser (cellophane/parser {:read animals-read})}))
;; Server-side rendering:
(def component (cellophane/add-root! animals-reconciler AnimalsList nil))
(dom/render-to-str component)
;; => "<div data-reactroot=\"\" data-reactid=\"1\" data-react-checksum=\"-1712681713\">
;; <h2 data-reactid=\"2\">Animals</h2>
;; <ul data-reactid=\"3\">
;; <li data-reactid=\"4\">1. Ant</li>
;; <li data-reactid=\"5\">2. Antelope</li>
;; <li data-reactid=\"6\">3. Bird</li>
;; <li data-reactid=\"7\">4. Cat</li>
;; <li data-reactid=\"8\">5. Dog</li>
;; <li data-reactid=\"9\">6. Lion</li>
;; <li data-reactid=\"10\">7. Mouse</li>
;; <li data-reactid=\"11\">8. Monkey</li>
;; <li data-reactid=\"12\">9. Snake</li>
;; <li data-reactid=\"13\">10. Zebra</li>
;; </ul>
;; </div>"
Because Cellophane's defui
generates Clojure records (which under the hood are Java classes), :require
ing other namespaces is not enough to use those components. Using :import
is also required, as demonstrated below:
(ns my-ns
(:require [other.ns :as other])
(:import [other.ns Component]))
This is only required until version 0.2.5
. Om Next components are now generated as plain Clojure functions which can be :require
d and :refer
ed by their name.
Because Cellophane component classes are plain Clojure functions which reify
some protocols, calling class
on a component instance won't return the actual component, but the anonymous class created by Clojure's reify
. Use cellophane/react-type
(which is also present in Om Next's public API) to get the actual component class.
Because JavaScript environments support adding arbitrary methods to an object's prototype, it's possible in ClojureScript to define methods not in protocols under the special Object
protocol. This is, however, not possible in JVM Clojure. As such, Cellophane only supports React's lifecycle methods defined under the Object
protocol.
;; this is only supported in ClojureScript
(defui MyComponent
Object
(some-function [this]
42))
Copyright © 2016 Ladder Financial, Inc.
Distributed under the Eclipse Public License (see LICENSE).
Contains adapted code from the following projects: