Giter Club home page Giter Club logo

derivatives's Introduction

Derivatives CircleCI cljdoc badge

Subscriptions distilled.

usage | comparisons | change log | API docs

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

Usage

[org.martinklepsch/derivatives "0.3.1-alpha"] ;; latest release

Derivatives of your application state can be defined via a kind of specification like the one below:

(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):

(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:

(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:

(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.

derivatives's People

Contributors

bago2k4 avatar chpill avatar cmdrdats avatar hkjels avatar livtanong avatar martinklepsch avatar npmcdn-to-unpkg-bot avatar plexus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

derivatives's Issues

Further ideas about `drvs` mixin

via #5 โ€” Currently the drvs mixin places a list of derivative ids in the component state and gets a list of their values when using react-drvs. I think this intermediate list is problematic for a few reasons:

  • You cannot use react-drvs with derivatives that have not been "included" using drvs, i.e. the ones included via (drv :some-thing).
  • The general benefit of using individual drv and react calls is that you can use them in your component code and they will only be dereferenced/used when the code is actually used (example below). In comparison when using react-drvs all derived values are dereferenced/reacted upon. In the example when the derived value for :b updates the component will not rerender. Whereas when react-drvs is used it would be re-rendered.
(defcs test < (drv/drv :a) (drv/drv b)
  [s]
  (if true
    (drv/react s :a)
    (drv/react s :b)))

I'm thinking a solution to both of these issues could be that react-drvs takes a list of dervative-ids similar to the drvs mixin. To make that usable it would also have to return a map.

(react-drvs (if true :a :b) :c (when false :d))

which would return a structure like

{:a 'value-of-a :c 'value-of-c}

This would make the destructuring a little less nice but overall seems like a more flexible approach.

@CmdrDats curious what you think. Others are also invited to weigh in of course. Maybe the flexibility isn't really needed and I'm just thinking to hard about potential issues. ๐Ÿ˜„

Multiple derivative-pools fail with same keys/source atom in spec don't work

In the example app we test both ways of injecting a derivative pool into the React context.

For both injections (via rum-derivatives and rum-derivatives*) there is a derivative pool created. The pool eventually creates watches on the source atoms using the keys in the spec. If the keys in this spec overlap the watch is overwritten instead of added. For this reason the watch key should be a composite of the key in the spec and a pool specific UUID or similar.

Parameterized Derivatives / Subscriptions

I'm trying to render a list of posts and what i need is to pass each post it's own data only that i have as a map inside the app state.
Now i know i can create a derivative like:
(drv/drv :posts)
to pass the whole list of posts, but what about the single post?
right now i can't do something like
(drv/drv :posts post-id) or like (drv/drv (keyword (str "posts-" post-id))).
The main problem is that i don't know the post IDs before i load so i can't create the derivative spec for each of them.
Passing all the posts to each component is not very good for performance since it re-render every time something change in the whole list of posts.

Thank you

derivatives unavailable in `will-unmount` if the latter is placed after the former.

Affects get-ref.

If I understand the source correctly, this is because the derivatives mixin always includes unmount code. You can call the custom will-unmount before the derivatives mixin, but as a project gets more complex, it could lead to code that is difficult to reason about. Something more explicit could be worth exploring.

I have two suggestions:

  1. Split the derivatives mixin into separate mount and unmount functions/vars
  2. Include an optional will-unmount hook into the d/drv mixin that will get called before the mixin executes its own unmount.

`did-remount` is not called when derivative changes

Hi Martin, i found something that i think it's an issue. When you have a component mounted and something changes a derivative associated with it, you would expect the did-remount hook to be called with the old and the new state.
did-remount is what Rum calls componentWillReceiveProps.
In my tests this doesn't happen but i think it should. Derivatives are props for the component, it should be alerted when they change.

Here is an example:

(def app-state (atom 0))

(def drv-spec
  {;; a source with no dependencies
   :app-state     [[] app-state]})

(rum/defcs sample < rum/reactive
  (d/drv :app-state)
  {:did-remount (fn [old-state new-state]
                            ;; this is never called
                            new-state)})
  [s]
  [:button
   {:on-click #(swap! app-state inc)}
    "Increment app-state"])

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.