Skip to content

sansarip/peanuts

Repository files navigation

Peanuts Logo

Peanuts

Clojars Project Build Status

Packing peanuts for decoupling Reagent Form-1 components from Re-frame subscriptions

deps.edn

peanuts/peanuts {:mvn/version "0.7.2"}

Leiningen

[peanuts "0.7.2"]

ToC

  1. Rationale
  2. Usage
    1. Interactive devcards
    2. fnc macro
    3. defnc macro
    4. Options
      1. Redlisting Args
      2. Greenlisting Args
  3. Known Limitations

Rationale

This bit is pretty opinionated, but I dislike using the below structure to define Reagent Form-1 components.

;; Method A

(defn my-component []
  (let [... bunch of re-frame subscriptions here ...]
    [:div "hiccup stuff"]))

The problem with the above example is that functions defined as such become impure, heavily dependent on the Re-frame subscriptions bound in their let-forms - coupling the components and the subscriptions. This makes it harder to create a library of components that you can share between projects, and it makes the components harder to test.

I prefer something like this...

;; Method B

(defn my-component [{:keys [... args ...]}]
  [:div "hiccup stuff"])

In my preferred method (Method B) the subscriptions would happen outside of the components (in a root component/view), and the data would just simply be passed in. The problem with my preferred method is that it can noticeably affect performance when you have nested components - due to Re-frame subscription mumbo-jumbo.

Enter Peanuts. Peanuts component macros are intended to wrap components implemented like Method B, turning them into components that behave like Method A without the performance drawback. The component will use any args passed in as is or subscribe to them if the args are valid subscription ids/vectors!

I've utilize this simple library in production to great extents, and it has really scratched an itch for me!

Usage

Image from Gyazo

An example of wrapping an existing Form-1 component

It goes without saying that you should have re-frame as a project dependency. You may also need to require it in the namespace(s) you use Peanuts.

The main ways to use peanut components are the fnc and defnc macros. For documentation on the older defc and fc macros, see the README here

fnc

Similar to fn

(ns my-ns
  (:require [peanuts.core :refer [defnc]]))

(def foo (fnc [n] [:p (str "Hello, " n "!")]))

defnc

Similar to defn

(ns my-ns 
  (:require [peanuts.core :refer [defnc]]))

(defnc foo [n]
  [:p (str "Hello, " n "!")])

See this little blurb if you wish to resolve defnc as a defn and fnc as an fn in IntelliJ with Cursive!

You can also import the clj-kondo config like so:

{:config-paths ["peanuts/peanuts"]}

Options

Both fnc and defnc accept an optional map as an argument that can dictate certain options explained below. In the case of defnc the map will also be applied as metadata on the defined name. You can also pass the options map as the second - or third if there's a docstring - argument.

There are older supported options available that you can read about here. But, I'm only supporting them for backward compatibility's sake and would advise against using those options!

Redlisting Args

The :redlist options are there for instances where you'd want certain args that coincide with valid subscription-identifiers/vectors to pass through without being rebound to their respective subscription values.

(defnc foo
  [adj n]
  {:redlist [adj]}
  [:p (str "Hello, " adj " " n "!")])

;; Or

(defnc foo
   [^:redlist adj n]
   [:p (str "Hello, " adj " " n "!")])

In the above examples, the adj parameter will always be redlisted/excluded from being rebound to subscription values.

You can also redlist args with metadata when calling the function/component.

(defnc foo
   [adj n]
   [:p (str "Hello, " adj " " n "!")])

;; You can also use :rl instead of :redlist
[foo ^:redlist [:my-adj] :my-name]

One thing to note with the above example though is that although the adj arg will be exempt from being rebound to a subscription value if a valid subscription identifier/vector is passed in, it won't change the amount of code the defnc macro emits. In contrast, omitting parameters via the :redlist (or :greenlist) options does result in less code being emitted - if you care about that!

In addition, you can redlist an arg once with metadata. This can be handy when you want to pass down a subscription-id/vector to a nested peanuts component. This is a similar behavior to using the :redlist option in the peanuts component definition.

(defnc bar
       [adj n]
       ;; adj will be rebound to subscription value
       [:p (str n " is an " adj " name!")])

(defnc foo
       [adj n]
       [:<> 
        ;; adj will be used as is e.g. [:my-adj]
        [:p (str "Hello, " adj " " n "!")]
        [bar adj n]])

;; You can also use :rl1 instead of redlist1
[foo ^:redlist1 [:my-adj] :my-name]

Greenlisting Args

An alternative to the :redlist option is the :greenlist option. If the :greenlist option is specified, then only those specified parameters will be candidates for being rebound subscription values.

(defnc foo
  [adj n]
  {:greenlist [n]}
  [:p (str "Hello, " adj " " n "!")])

The above example is equivalent to the example in the Redlisting Args section. In the example above, only the greenlisted n parameter can be rebound to a subscription value. One thing to note is that the :redlist option always takes precedence over the :greenlist option in odd cases where both options are defined with conflicting args.

Known Limitations

This library doesn't fully replicate all the bells and whistles of the defn macro or the fn form.

There are some known limitations:

  • Function overloading isn't supported
  • fnc does not support naming e.g. (fnc d []) does not work
  • Function constraints e.g.
(defn constrained-sqr [x]
    {:pre  [(pos? x)]
     :post [(> % 16), (< % 225)]}
    (* x x))

It's not that the above limitations can't be fixed; I just haven't run into a necessary use case yet. If there's demand to fix any of the mentioned limitations, I will do it!