GithubHelp home page GithubHelp logo

lilactown / flex Goto Github PK

View Code? Open in Web Editor NEW
75.0 7.0 2.0 1.16 MB

flex is a reactive signal library for Clojure(Script)

License: Eclipse Public License 2.0

Clojure 100.00%
clojure clojurescript reactive reactive-programming reactivity state-management dataflow signals

flex's Introduction

flex

flex is a library for building computations using signals in Clojure(Script). It gets its inspiration from reagent and its lineage (reflex, KnockoutJS, S.js) as well as SolidJS.

Install

Using git deps

town.lilac/flex {:git/url "https://github.com/lilactown/flex"
                 :git/sha "346bad039b560a62529a39af66e0afe35565b125"

Example

(ns my-app.counter
  "A simple reactive counter"
  (:require
   [town.lilac.flex :as flex]))

;; a state container that can be changed manually
(def counter (flex/source 0))

;; a computation that updates when its dependencies change
(def counter-sq (flex/signal (* @counter @counter)))

;; an effect that runs side effects when its dependencies change
(def prn-fx (flex/listen counter-sq prn))

(counter 1)
;; print: 1

(doseq [_ (range 5)]
  (counter inc))

;; print: 4
;; print: 9
;; print: 16
;; print: 25
;; print: 36

;; batch updates in transaction to avoid computing signals/effects until the end
(flex/batch
 (counter inc)
 (counter inc)
 (counter inc)
 (counter inc))

;; print: 100

;; stop the effect from running and any dependent signals calculating
(flex/dispose! prn-fx)

(counter inc)

;; nothing is printed

Features

  • JS and JVM platform support
  • Reactive sources, signals and effects (town.lilac.flex)
  • Functional transduce/transform/reduce (town.lilac.flex.xform)
  • Memoized signal functions (town.lilac.flex.memo)
  • add-watch & remove-watch support (town.lilac.flex.atom)
  • Batching
  • Error propagation to consumers
  • Async support on JS (town.lilac.flex.promise)
  • Multiplexing / multithreaded scheduling on JVM

Differences from reagent

Platform support

flex supports Clojure on the JVM. Reagent is ClojureScript (JS) only

Eager vs lazy

flex computes "live" signals eagerly after a source has changed, and uses a topological ordering to ensure that calculations are only done once and avoid glitches. Reagent propagates changes up the dependency chain and only re-calculates when dereferenced, which can avoid unnecessary work in some instances but can also lead to glitches.

Batching

When inside of a batched transaction, flex does not compute any dependent signals until the transaction has ended, even if the signal is explicitly dereferenced. When reagent is batching updates, it will calculate the result of a reaction with the latest value if it is dereferenced.

Errors

If an error occurs inside of a flex transaction, all changes are rolled back and no signals are updated. If you are updating a group of ratoms in reagent, if any error occurs in between updates then you can end up in a state where some of the ratoms are up to date and others are not.

If an error occurs in a flex signal, the error is propagated to consumers so that it can be handled by an effect that has an on-error callback attached. Reagent reactions do not propagate errors and simply fail silently, leaving your app in a broken state if an exception is thrown.

Scheduling

flex does all changes, computations and effects synchronously by default. Reagent schedules some effects asynchronously using requestAnimationFrame.

Nested effects

Effects can be nested within each other, and when the outer effect is disposed it will dispose of all inner effects.

Scope

flex only handles reactive computations and has no external dependencies. Reagent bundles together its reactive computations with additional functionality to build web apps in the browser, and depends on React.js for this.

Related projects

Demos & examples

helix-flex: A simple project to setup and evaluate flex with React (helix).

License & Copyright

Copyright 2022 Will Acton. Released under the EPL 2.0 license

flex's People

Contributors

lilactown avatar rafaeldelboni avatar rschmukler 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

flex's Issues

Sample Project using this and release on clojars?

Hey first of all thanks for all your work on OSS for the cljs community I'm a Helix heavy user and is such a nice lib.

ATM I'm building a new side project and I'm evaluating tools to manage global state and searching for signal libraries I found this one.

I have two questions:

Do you have any sample project using this lib? I would like to see how is the ergonomics of it in a web/ui app doing async IOs and all that stuff :)

Since is doesn't have an official release, should I be careful with it's current state or big api changes?

Regards

flex.promise/resource that accepts fetcher function with multi args

Would you be interested in a PR with a new (breaking) version of flex.promise/resource that accepts a fetcher function that could be called with multiple arguments?

I'm using one in a personal project and I'm liking it so far.

You can check it here, I even rewrote your tests to validate the idea and is a simple way to show you how it works (ignore the promesa stuff, they are enabling async tests work in jsdom environment)

The breaking change is that it doesn't call itself in the initialization

Lazy sequences in signals can lead to subtle bugs

From #5

(def src (f/source 5))

(def A (f/signal (* @src @src)))

(def B (f/signal (map #(+ @A %)
                      (range 5))))

(def fx (f/effect [] (prn @B)))

This currently throws an exception passing 0 args to max because no dependencies were found when running B.

Fixing this will still leave subtle bugs in programs that return lazy seqs inside of signals.

babashka support

This is currently blocked by the fact that babashka does not allow extending Java interfaces, e.g. clojure.lang.IFn

Allow testing disconnected signals

We should provide some way of testing disconnected signals at the REPL and in tests. See #5

Proposal

My current idea is to implement the following behavior:

(def src (source 1))
(def A (signal (inc @src))
(def B (* @B @B))

@B ;; => 4
;; prints "WARNING: Signal dereferenced outside of reactive context"

a var could be set to quiet this while running tests

(set! flex/*warn-nonreactive-deref* false)

(deftest B-test
  (is (= 4 @B)))

Initial stumbling blocks

Hi,

Sorry I'm getting back to you so late. I appreciate the thoughts you shared with me here:

https://old.reddit.com/r/Clojure/comments/11723ea/is_there_a_reframecljfxlike/

I just tried to convert a little plotting program of mine to use flex and I've hit a few initial stumbling blocks. I only really mean this as to share an initial-impression/user-experience - and not criticism. It's very likely "i'm holding it wrong" and misunderstanding how to use flex.

  1. Signals can't be inspected at the REPL
    Just working off the example in the README. You hook up counter-sq to your counter atom. When counter updates, counter-sq updates under the hood. However as far as I can tell, there is no way to actually inspect counter-sq at the REPL. It just returns a :town.lilac.flex/init. You can hook up a listener and print the value, but it's clunky and only prints a single time on change. tbh, I expected it to outwardly continue to behave transparently as a integer - but at the very least I'd want to be able to peak at its value at the REPL :)

  2. Not super clear how to chain signals
    I obviously wants to go a step further from the basic example and chain my signals. Leverage diamond dependencies and so on.. Since I can't dereference them, I'm not super sure how to do that? I can listen to a signal and create a new signal - but that seems a bit clunky. I don't think it will do the diamond dependency magic that way as well?

  3. signal wrapping everything..
    Basically any variable that derefs a flex/source will in effect be a signal. So I'm envisioning you end up with a ton of this in your code:

(def my-var
    (flex/signal
    ....
...

Maybe just aesthetic but it'd be a bit clearer with a macro

(defs my-var
    ... 

Because you don't exactly have a static variable nor really a defn function. That said, I get the desire to avoid macros :))

  1. No quick and easy way to subscribe to map values
    In the CLJFX model the equivalent of signals are split into sub-val and sub-ctx. The latter is a memoized function call - a bit similar to your signal. The former is a convenience function that allows you to subscribe to map values

So as I started to massage my code to use flex, my flex/source atom looked something like this:

(def
  params
  "The state atom"
  (flex/source
    {:input-dir   "./data/gauge/"
     :id-2-name   {1645240 5
                   8373316 3
                   7598252 4
                   1683136 2
                   8371400 1
                   1641660 6
                   7598984 7}
     :inside-d18o  "irregular-cave-samples.csv"
     :outside-d18o "manual-rain-samples.csv"
     :plot-height 400
     :plot-width 1000
     :blah        "blah"}))

From the API it looks like in flex I need to create a signal for each an every key @_@ so that when a part of the map gets updated I get the appropriate update (that can propagate to all the signals). Otherwise my whole DAG gets recomputed on each map update (well I assume it'll terminate when identical values are returned). Ergonomically this is a bit of a mess. Basically if you have a .. simple? (finite/non-lazy) built in datatype as a source, you can be sure you'll automatically want signals for every single elements

in CLJFX sub-val is limited to first level map keys - but ideally then should be able to generate a signal on deeper values

Anyway, those are my first thoughts :))

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.