https://github.com/martinklepsch/derivatives
🌱 Your companion to create derived values from a single source (atom)
https://github.com/martinklepsch/derivatives
clojurescript dataflow reactive rum
Last synced: 7 days ago
JSON representation
🌱 Your companion to create derived values from a single source (atom)
- Host: GitHub
- URL: https://github.com/martinklepsch/derivatives
- Owner: martinklepsch
- License: mpl-2.0
- Created: 2016-07-04T19:10:16.000Z (almost 9 years ago)
- Default Branch: master
- Last Pushed: 2019-05-17T18:44:44.000Z (almost 6 years ago)
- Last Synced: 2025-05-05T22:16:06.064Z (7 days ago)
- Topics: clojurescript, dataflow, reactive, rum
- Language: Clojure
- Homepage:
- Size: 126 KB
- Stars: 112
- Watchers: 10
- Forks: 16
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGES.md
- License: LICENSE
Awesome Lists containing this project
README
# Derivatives [](https://circleci.com/gh/martinklepsch/derivatives) [](https://cljdoc.xyz/d/org.martinklepsch/derivatives/CURRENT)
*Subscriptions distilled.*
[usage](#usage) | [comparisons](#comparisons) | [change log](CHANGES.md) | [API docs](https://cljdoc.xyz/d/org.martinklepsch/derivatives/CURRENT)
**A note on terminology:** There are a lot of things with similar meanings/use-cases around: subscriptions, reactions, derived atoms, view models.
I'll introduce another to make things even worse: *derivative*. A *derivative* implements `IWatchable` and it's value is the result of applying a function to the value of other things (*sources*) implementing `IWatchable`. Whenever any of the *sources* change the value of the *derivative* is updated.## Why this library
Let's assume you're hooked about the idea of storing all your application state in a single atom (`db`). (Why this is a great idea is covered elsewhere.)
Most of your components don't need the entirety of this state and instead receive a small selection of it that is enough to allow the components to do their job. Now that data is not always a subtree (i.e. `(get-in db ...)`) but might be a combination of various parts of your state. To transform the data in a way that it becomes useful for components you can use a function: `(f @db)`.
Now you want to re-render your application whenever `db` changes so the views are representing the data in `db`. You end up calling `f` a lot, and remember, `f` has to do all the transformation for all components that **could** be rendered on the page, pretty inefficient!
To optimise we can create *derivatives* that contain data in shapes ideal to specific components and re-render those components when the *derivative* supplying the data changes.
These *derivatives* may depend on other *derivatives*, all ultimately leading up to your single `db` atom. To keep things efficient we only recalculate the value of a *derivative* when any of it's *sources* changes.
The intention of this library is to make the creation and usage of these interdependent references (*derivatives*) simple and efficient.
A secondary objective is also to achieve the above without relying on global state being defined at the namespace level of this library. (See re-frame vs. pure-frame.)
### What this library helps with
- transform `db` into shapes suited for rendering (a.k.a. view models)
- managing a pool of *derivatives* so only needed *derivatives* are created and freed as soon as they become unused (currently Rum specific)
- server-side rendering (to some degree)### What this library doesn't help with
- Ensuring the required data is in `db` (server/client rendering)
- [Parameterized Subscriptions](#why-no-parameterized-subscriptions)## Usage
[](dependency)
```clojure
[org.martinklepsch/derivatives "0.3.1-alpha"] ;; latest release
```
[](/dependency)*Derivatives* of your application state can be defined via a kind of specification like the one below:
```clojure
(def *db-atom (atom 0))(def drv-spec
{;; a source with no dependencies
:db [[] *db-atom]
;; a derivative with a dependency
:inc [[:db] (fn [db] (inc db))]
;; a derivative with multiple dependencies
:as-map [[:db :inc] (fn [db inc] {:db db :inc inc})]}
```A specification like the above can be easily turned into a map with the same keys where the values are *derivatives* (see `org.martinklepsch.derivatives/build`).
Also it can be turned into a registry that can help with only creating needed derivatives and freeing them up when they become unused (see `org.martinklepsch.derivatives/derivatives-pool`).
> What follows is Rum specific and this library has a dependency on Rum but this pattern could be used with old Om apps, or even Reagent's reactions. I'm very open to changes in that direction.
In a Rum component tree you might use *derivatives* as follows (assuming `*db-atom` and `drv-spec` as above):
```clojure
(rum/defcs derived-view < rum/reactive (d/drv :inc) (d/drv :as-map)
[s]
[:div
[:p ":inc " (-> (d/react s :inc) pr-str)]
[:p ":as-map " (-> (d/react s :as-map) pr-str)]])(rum/defc app < (d/rum-derivatives drv-spec)
[spec]
[:div
[:button {:on-click #(swap! *db-atom inc)} "inc"]
(derived-view)])
```The `rum-derivatives` mixin adds two functions to the React context of all child components: one to get a *derivative* and one to release it. The `drv` mixin adds hooks to your components that do exactly that and allow you to access the derivatives via component state.
## Comparisons
#### Plain `rum.core/derived-atom`
Rum's derived-atoms serve as building block in this library but there are some things which are (rightfully) not solved by derived-atoms:
- Creation of interdependent *derivative* chains and
- a mechanism to only create actually needed derived-atoms.A small code sample should illustrate this well:
```clojure
(def *db (atom {:count 0})) ; base db(def *increased
(rum/derived-atom [*db]
::increased
(fn [db]
(inc (:count db)))))
(def *as-map
(rum/derived-atom [*db *increased]
::as-map
(fn [db incd]
{:db db :increased incd})))
```compared with the way this could be described using *derivatives*:
```clojure
(def *db (atom {:count 0}))(def spec
;; {name [depends-upon derive-fn]}
{:db [[] *db]
:increased [[:db] (fn [db] (inc (:count db)))]
:as-map [[:db :increased] (fn [db incd] {:db db :increased incd})]})
```The benefit here is that we don't use vars to make sure the dependencies are met and that we provide this information in a way that can easily be turned into a dependency graph (data FTW) which will later help us only calculating required *derivatives* (done by `derivatives-pool`). In comparison the first snippet will create derived-atoms and recalculate them whenever any of their dependencies change, no matter if you're using the derived-atom in any of your views.
#### Re-Frame Subscriptions
The way they work Re-Frame's dynamic subscriptions are not much different from the approach chosen here, they vary in two ways however:
- In Re-Frame you can do `(subscribe [:sub-id "a parameter"])`, with *derivatives* you can't. Instead these parameters need to be put into `db` and be used (potentially via another *derivative*) from there.
- In Re-Frame subscriptions may have side-effects to listen to remote changes etc. This library does not intend to solve this kind of problem and thus side effects are discouraged.#### Why no parameterized subscriptions?
In my personal experience a lot of non-idiomatic, non performance
optimal re-frame use comes from having subscriptions in every corner
of the code. Parameterized subscriptions enable this even more.While a bandaid more than a solution the lack of parameterized
subscriptions in Derivatives is meant to discourage ad-hoc, throwaway
subscription use and instead encourage thoughtful reshaping of data
from your DB into a form suitable for rendering.## Contributing
Feedback and PRs welcome.
Tests can be run with `boot --source-paths test test` or `boot -s test test`.
--
**License:** MPLv2, see `LICENSE`.