Skip to content

Component & om.next shared

Petter Eriksson edited this page Dec 6, 2017 · 4 revisions

Using Stuart Sierra’s Component library worked well for us on the server. Having the entire system in a map passed to our endpoints and om.next/parser functions (read & mutate) made our code well structured. It also made it easy to create fake implementations for our external dependencies, which made it easy to create a demo version of our app and open source it.

We took the same concept to our client, sticking the entire “client system map” or “client components” in the om.next reconciler’s :shared map. This gave us the same power to select which version components to use at dev/demo/production time. The :shared map is passed to each ui component and it’s also available for the om.next/parser functions.

We didn't use use the Component library for our :shared map. We created our own small library which also has the property of instantiate the components lazily (whenever they are used). This works well with ClojureScript’s module loading system, because only the namespaces using the components in the :shared map will reference the component’s namespace, and ClojureScript will try to move these components into the smallest possible module.

See implementation: eponai.common.shared

All the components created by this namespace are cached and meant to be used as singletons.

Usage

Using a shared component

Here we're defining a UI component, getting a component and using the component's namespace to call a method on that component.

(ns eponai.web.ui_component
  (:require
    [om.next :as om :refer [defui]]
    [om.dom :as dom]
    ;; Requiring the shared namespace so we can access the components.
    [eponai.common.shared :as shared]
    [eponai.web.login :as login]))

(defui UiComponent
  Object
  (render [this]
    ;; Getting the login component by its unique key :shared/login.
    ;; The first argument to shared/by-key can either be a component or a reconciler.
    (let [login-component (shared/by-key this :shared/login)]
      (dom/div nil
        (dom/button #js {:onClick #(login/prompt-login! login-component)}
                    "Login!")))))
      

Creating a shared component

To create a shared component, one implements a multimethod defined in the eponai.common.shared namespace, which is passed the reconciler, a unique key to identify the component by, and which version of the component to get.

Code for a new component:

(ns eponai.web.hello_component
  (:require 
    [eponai.common.shared :as shared]
    ;; Include other interesting components if the component needs them.
    [eponai.web.firebase :as firebase]))

(defprotocol ISayHello
  (say-hello [this language] "Returns a version of Hello in the specified language"))

(defmethod shared/shared-component [:shared/hello-component :env/dev]
  [reconciler k env]
  (reify ISayHello
    (say-hello [_ _]
      ;; Ignores the language, just returns a default string.
      "Hi dev")))

(defmethod shared/shared-component [:shared/hello-component :env/prod]
  [reconciler k env]
  ;; We're passed the reconciler to be able to use any state or other shared component
  ;; If we need to.
  (let [firebase (shared/by-key reconciler :shared/firebase)]
    (reify ISayHello
      (say-hello [_ language]
        ;; Here we're tracking every call to say-hello in firebase as well.
        (firebase/update-in firebase 
                            (firebase/path :demo/say-hello {:language language})
                            inc)
        (condp = language
          ::swedish "Hej"
          ::english "Hi"
          "Yo")))))

Resetting stateful components

If you're using figwheel and you want to be able to reset components to their initial state on reload, call the eponai.common.shared/clear-components! function. This will reset the component cahce and return a new component when the eponai.common.shared/by-key is called.