reflux / refluxjs

A simple library for uni-directional dataflow application architecture with React extensions inspired by Flux
BSD 3-Clause "New" or "Revised" License
5.36k stars 330 forks source link

Make reflux isomorphic friendly #144

Closed spoike closed 9 years ago

spoike commented 9 years ago

Feedback I receive around suggest that reflux needs to be more friendly for isomorphic projects, i.e. same actions and stores can be run both client-side and server-side.

The goal with this issue is how can we achieve this in reflux and still keep a simple API?

Suggestions and PR's are welcome.

appsforartists commented 9 years ago

@geekyme You don't need a new function for that stuff. You'll want to initialize your stores before React renders, so you don't have to worry about the components trying to listen on the server side (as they won't exist yet when the stores first trigger).

On the client, do what you already do. On the server, create the store, trigger an action that kicks off XHR, load the result into the store, and when the store triggers, start rendering.

I'll post an example next week. There's room for Reflux improvement in this direction for sure (the ability to map actions to the stores they trigger, and perhaps a store.setState convention both come to mind), but there's nothing preventing you from writing an isomorphic Reflux app using the code that's on npm right now.

geekyme commented 9 years ago

@appsforartists if I load the result into the store on the server, this might lead to state leakages between concurrent requests wouldn't it?

dashed commented 9 years ago

@geekyme The consensus is that you instantiate the stores per request.

appsforartists commented 9 years ago

One of the things I've come to realize while working on this PR is that the state that needs to be serialized from the server to the client needs to live at a known location (I've been using this.state), and that for simple stores, that's the same variable that needs to be broadcast back to the handlers.

Would it make sense to have a default for this.trigger, such that if it's called without arguments, it's equivalent to calling this.trigger(this.state)?

We'll need to adopt a convention like this.state to make sure that the Reflux ecosystem stays isomorphic-friendly and intracompatible. Having it subtly reinforced by Reflux itself could be a good thing.

geekyme commented 9 years ago

I thought we would need to get data serialized on the server transmitted via an inline script tag?

I was wondering how Reflux can automatically dehydrate (to serialize data) and hydrate (to reset its store to the serialized data)

Sent from my iPhone

On 22 Dec 2014, at 4:00 am, Brenton Simpson notifications@github.com wrote:

One of the things I've come to realize while working on this PR is that it the state that needs to be serialized from the server to the client needs to live at a known location (I've been using this.state), and that for simple stores, that's the same variable that needs to be broadcast back to the handlers.

Would it make sense to have a default for this.trigger, such that if it's called without arguments, it's equivalent to calling this.trigger(this.state)?

We'll need to adopt a convention like this.state to make sure that the Reflux ecosystem stays isomorphic-friendly and intracompatible. Having it subtly reinforced by Reflux itself could be a good thing.

— Reply to this email directly or view it on GitHub.

appsforartists commented 9 years ago

Presently, Reflux does not have a constructor; however, it would be massively helpful to have one to make it easier to create isomorphic-friendly stores. To that end, I've prototyped one and tested it in an isomorphic app.

It takes a single argument, a dictionary of store and action definitions. Following up on @willembult's product catalog example, you might have something that looks like this:

var refluxDefinitions = {
  "Products":       {
                      "actions":      [
                                        "getProduct"
                                      ],

                      "store":        Object.assign(
                                        {
                                          "init":                           function () {
                                                                              this.state = {};
                                                                            },

                                          "onGetProduct":                   function (productKeyName) {
                                                                              if (this.state[productKeyName]) {
                                                                                this.trigger(this.state);

                                                                                if (!this.settings.CUSTOM_SETTINGS["ACTIONS_UPDATE_CACHED_DATA"]) {
                                                                                  return;
                                                                                }
                                                                              }

                                                                              this.getProductFromAPI(productKeyName).then(
                                                                                productModel => {
                                                                                  this.state[productKeyName] = productModel;
                                                                                  this.trigger(this.state);
                                                                                }

                                                                              ).catch(
                                                                                error => console.error(error.stack)
                                                                              );
                                                                            },

                                          "getProductFromAPI":              function (productKeyName) {
                                                                              return this.getFromAPI(`products/${ productKeyName }`);
                                                                            },
                                        },

                                        require("./mixins/getFromAPI.js")
                                      )
                    },

 "CurrentProduct":  {
                      "dependencies": {
                                        "stores":   [
                                                      "Products"
                                                    ]
                                      },

                      "actions":      [
                                        "getProduct"
                                      ],

                      "store":        {
                                        "init":                           function () {
                                                                            this.state = null;

                                                                            this.listenTo(
                                                                              this.parent.stores.Products,
                                                                              this.onProductsUpdated
                                                                            );
                                                                          },

                                        "onGetProduct":                   function (productKeyName) {
                                                                            this.productKeyName = productKeyName;

                                                                            var product = this.parent.stores.Products.state[this.productKeyName];

                                                                            if (product) {
                                                                              this.state = product;
                                                                              this.trigger(this.state);
                                                                            }
                                                                          },

                                        "onProductsUpdated":              function (products) {
                                                                            if (this.productKeyName) {
                                                                              this.state = products[this.productKeyName];
                                                                              this.trigger(this.state);
                                                                            }
                                                                          },
                                      },
                    }                 

};

For each definition, the value in actions is passed to Reflux.createActions and the value in store is passed to Reflux.createStore. Everything in actions is automatically listened to by the store its attached to (using listenables), and the same action can be listened to by multiple stores.

The above example also shows how one store can depend on another. A store can access the Reflux instance it's part of with this.parent and can declare which siblings it expects with dependencies. Whenever the Products triggers, CurrentProduct will too (in onProductsUpdated).

I've been designing an isomorphic app server with React and ReactRouter. I've posted an early preview for your guys' benefit, but I'm not announcing it until the end of January (so please don't go posting it on Hacker News or Reddit - it isn't ready yet).

With that in mind, here's how I Reflux on the server, and here's the client.

They each follow the same steps:

1) Instantiate a new Reflux (once on the client, but for each request on the server) with the definitions dictionary described above. On the client, also rehydrate the state passed down from the server.

2) callActionsForRouterState: This is really cool. You pass it a list that maps each ReactRouter parameter name to the action and store it should trigger:

[
  // no parameterName, so it's always triggered
  {
    "actionName":     "getCategoryTree",
    "storeName":      "CategoryTree",
  },

  // only triggered if categoryKeyName is present on routerState
  {
    "parameterName":  "categoryKeyName",
    "actionName":     "getProductsInCategory",
    "storeName":      "CurrentProductIndex",
  },

  // won't resolve until the isReady test passes
  {
    "parameterName":  "productKeyName",
    "actionName":     "getProduct",
    "storeName":      "CurrentProduct",
    "isReady":        product => product.detailLevel === ProductModel.DetailLevel.LISTING,
  }
]

and it will wait for them all to load before rendering continues. There's still work to be done here (I need to come up with a convention that maps user-specific state to actions just like we can already map route-specific state), but it's the direction this API should go. All you need to worry about is mapping the state to actions, and the server knows how to wait until your data is all ready to proceed.

3) Render, passing the reflux instance down as context. I've taken the liberty of reimplementing Reflux.connect in a context-aware way. Then, all your Handlers have to do is mix in Ambidex.mixinCreators.connectStoresToLocalState and they can access your store as this.state.storeName.


In order to link store state to handler state, we need to know what would be sent by this.trigger. As such, I think we should deprecate the ability to pass a variable into this.trigger and instead have it introspect the value to be sent from its own store.

We also need to know what needs to be serialized to be passed from the server to the client. In the prototype, I'm presuming it's called this.state, but I also presume this.trigger(this.state). These two concerns should be separated.

Off the top of my head, there are two ways to solve this:

1) Require everything that needs to survive dehydration to be placed on this.state and define a getter that allows you to dress up what gets triggered:

"getValue":     function () {
                  return this.state.thePublicData;
                }

and this.trigger effectively becomes:

this.trigger(
  this.hasOwnProperty("getValue")
    ? this.getValue()
    : this.state
)

, or 2) Require that whatever should be triggered is stored in a known place (like this.state) and have hydrate iterate over all the store's own properties and backup anything that doesn't look like a function.


Let the bikeshedding commence. As far as I'm concerned, this is all a prototype. I fully expect someone to pipe in and say "what about this concern" that will require this stuff to be thought out again. In the mean time, I hope this is helpful. After a week of massive experimentation, I'm excited to finally have something to show.

Merry X-Mas everybody!

:snowman: :santa: :gift: :christmas_tree: :snowman:

appsforartists commented 9 years ago

@spoike I used Lazy to prototype the constructor for clarity and development speed, but we can refactor it to remove the dependency if there's a concern about bloating the payload.

appsforartists commented 9 years ago

I thought about it a bit this morning.

Since Stores can depend on one another, you can model the current user as a store that the other stores depend on. When CurrentUser is instantiated on the server, it can check the cookies to see which user to represent. On the client, you could have an action login(username, password) that tells the store to go fetch a user from the server.

You'd need a way for the store to access the current session, but otherwise it seems sound. It could be as simple as having the server check each store for an updateFromCookie method and passing in the request's cookie when it finds one.


In actionsFromRouterState, parameterName currently only accepts a String, but it should also accept an Array that specifies which parameters become action arguments and what order they appear in. I'm also amenable to having a getActionArgArray method that, if specified, will receive the relevant inputs (routerState, maybe cookie) and will return the arguments that an action should be called with. It would be an advanced override for the simple parameterKeyName.

So, there could be three ways to describe how to call an action:

"parameterName":      "onlyParam" // action(params[onlyParam])

"parameterNames":     ["param1", "param2"] // action(params["param1"], params["param1"])

"getActionArgArray":  (routerState) => [routerState.query["magicThing"], routerState.params["otherThing"]]  // action(routerState.query["magicThing"], routerState.params["otherThing"])

I really like the first two. The third might be nice-to-have for complex cases (and gives us a way to access cookies, if we decide they should be accessible to the actions).

geekyme commented 9 years ago

interesting idea about https://github.com/appsforartists/Ambidex/blob/master/src/callActionsForRouterState.js. Could this be better implemented with a waitOn attribute on react components?

var Test = React.createClass({
    ...
    waitOn: [Actions.magicMissile, Actions.fireBall]
    ...
});

waitOn will take in an array of functions / actions to call before the component is being rendered. So this gives room for us to do things like rehydrating stores maybe?

perhaps on the server waitOn could be in charge of dehydration and serializing into a javascript object.

Then on the client side waitOn can grab this serialized object and rehydrate the stores?

appsforartists commented 9 years ago

@geekyme:

The React component lifecycle is pretty well defined. I don't think you can block a render pass, so I don't know that you'd be able to implement a waitOn mixin as you've described.

appsforartists commented 9 years ago

I've posted a sample project

gpbl commented 9 years ago

@appsforartists thank you for your work, I was diving into it these days and it's really interesting. I was however a bit overwhelmed by its complexity. My hope was to keep reflux because its simplicity, otherwise I'd go for fluxible-app.

I tried then to solve the issue using the "old" way and keeping the flux pattern out from the server. In fact, on the server it is just about data, nothing should listen to stores. The app eventually would just hydrate the stores on the client.

This way, I could just work with the route handlers and their own getInitialState(). An isomorphic app is strictly related to the router handlers, so here for component i mean the route handler:

With a mixin and the use of React context, I could abstract the logic enough to make it clean and reusable.

Since I'm skipping flux on the server, there's only one part not fitting very well:

On the client, in fact, I rely on reflux actions and store handlers to request and consume the data. On the server, data must come from a different part: for each route handler, I defined a static method telling exactly which stores are in the game, what data should they use, and how to retrieve it. The server-side getInitialState would just consume this data, without even seeing the stores.

Now, if reflux could help at least in this part... for example, now on the server I make a direct request to the API and consume the response. On the client I run actions doing the same thing, but there a store is consuming the response. It would be nice to run the same actions also server-side, yet having something else listening to it: not a store, but an object running in the request context, before the renderToString.

The server app would say: hey reflux, for this route I need the results of these actions! When you are on the client, remember to hydrate these results to these stores. Then the server would look at the route handler and yell: here, use this data for initializing the app!. The server would take in fact the actions responses and pass them to the server-side route handler, and then dehydrate the stores for the client. Once on the client, reflux stores will notice that dehydrated data is available and will consume it for theirs getInitialState part.

appsforartists commented 9 years ago

What part of this seemed too complicated? There's definitely room for improvement, but to my eyes it doesn't seem any more complicated than Reflux itself.

I'm concerned that if you don't run Reflux on the server, you're either losing the benefit of isomorphism or duplicating logic.

gpbl commented 9 years ago

yes @appsforartists don't get me wrong, by meeting your same challenges now I understand better the various approaches. That part was clear. Where things get more complex is in the ambidex part: if I understood it right, it is like an abstraction layer over reflux.

When skipping reflux on the server yes, you must duplicate the logic for getting data. Hence keeping that part simple (and reusable by the reflux actions) is very important. For now it is a compromise I can accept since it works with the status quo :-)

appsforartists commented 9 years ago

Ambidex is a web server for isomorphic React apps. You would completely replace your Express dependency with it.

I still need to put some docs together, but at a high level, you just make a settings file that tells it:

From there, it will do all the hard part for you.

I think you're confused because of the Ambidex.mixinCreators.connectStoresToLocalState call in the example. Since Ambidex instantiates (hence owns) your Reflux instances, you have to ask it to return them to you. You can either use this.getRefluxStore/this.getRefluxAction, or use the mixin (which serves the same purpose as Reflux.connect).

It's definitely a work-in-progress, but you are welcome to use it. It's the basis for all the work I do for eBay's Mobile Innovations lab.

If you don't want to replace your app server, you could also just import it to steal the Reflux-instantiating code:

appsforartists commented 9 years ago

@influx6, I think of Stores as representing a variable you'd want to track in state. For instance, if you wrote a component that depended on this.state.bikes, then you'd have a Bikes store, where Bikes.state === showBikesComponent.state.bikes.

It gets a bit more complicated than that when you start talking about serialization because you might have other internal properties to manage. (For instance, you might have a CurrentBike store who emits a BikeModel as its state but also needs to keep track of which keyName to look up in the Bikes store.) To serialize the store correctly, you'd need to save/restore both state and keyName.

influx6 commented 9 years ago

Sorry for my hast,felt the question might have been abit simplified, indeed the complexity does grow, just as explained.

influx6 commented 9 years ago

Indeed, the inter-dependencies of stores and their connections with others,I think on the context of serialization, things should be flatten as much as possible,just like a db document that allows embeddement, store serialized data should contain enough information corresponding to the components and keynames associated with them. Just like one would do in a couchdb or mongodb document, that should clear our heads when it comes to dealing with the complexity of dependencies

influx6 commented 9 years ago

Naturally this will enforce that onces the server grabs the store data,they should be immutable per request, that is values dont change between same component embbedment on the first request from the server, if data changes this propagates along per request,i.e when a new client calls for the application, it gets the latest hot data, these ensures consistency across stores on that request load!

influx6 commented 9 years ago

But the central issue here is Isomorphism and how do we do it, to simplify our lives!. Lets assume the perfect world on flat dependency i.e as stated when the client request the pages from the server, the serialized data being sent for each store contains the necessary keyname and component reference links/address. How do we do it so simply that allows us to equal render on server and sent that off. My probably naive idea was to create a virtual-dom capable of running both server and client, and allow that to render the first server page load which is sent to client, but these requires in the context of reflux to work on the server. A solution would be to have reflux run on the server, and have that state serialised and sent to the client which then deserializes and continues from there.

appsforartists commented 9 years ago

It's not that simple, because things work in a system.

Imagine you have two backend calls going, one to get the details of a bike, and one to get a list of bikes (with fewer details). You wouldn't want to presume that just because CurrentBike had a value, it was ready to serve. Maybe it was populated by the /bikes/all/ call, but the /bike/1045/ call hasn't returned yet, so you have some data about that bike, but not the details you need.

The right thing to do is to test the value of the store to make sure it passes an acceptability test. When all the stores are acceptable, render and return.

influx6 commented 9 years ago

Oh yes,indeed after thinking about it, I realised your context thinks of the embedded version as stored in the server, the approach or idea rather is that the embedded document is generated per request from the internal request and then rendered up by the server and sent down, remember the only thing we do render the initial current state, eg like your examples of two request for cars, /app/cars and /apps/1045, intently on first request or expending on request response type, first renders the page after getting the data from the database , sends this rendered payload to the client,where the client then continues from, but it requires as I said that reflux work on the server. Every request must be considered a fresh install not a continuation from the last,hence why I said the current rendered data is Hot!, its recent data not based on a previous operation,even if within the scoped of two request in the same app,after first render, its all json cause the client takes over

appsforartists commented 9 years ago

One thing to remember with the callActionsForRouterState model: it will wait for the results of any parameters it knows how to resolve.

For instance, consider this URL: /manufacturers/soma/bikes/juice/ and these routes:

    <Route
      path    = "/manufacturers/:manufacturerID/"
      handler = { require('./bike-index/components/ManufacturerDetails.jsx') }
    />
    <Route
      path    = "/manufacturers/:manufacturerID/bikes/:bikeID/"
      handler = { require('./bike-index/components/BikeDetails.jsx') }
    />

    // ------ 

    var refluxActionsForRouterState = [
    {
      "parameterName":  "manufacturerID",
      "actionName":     "getManufacturer",
      "storeName":      "CurrentManufacturer",
    },

    {
      "parameterName":  "bikeID",
      "actionName":     "getBike",
      "storeName":      "CurrentBike",
    },
  ];

Even though BikeDetails probably doesn't care about the results of getManufacturer, it will end up waiting for it before rendering because it matched manufacturerID.

You can solve this by prefixing any parameter names you don't care about with a _:

    <Route
      path    = "/manufacturers/:manufacturerID/"
      handler = { require('./bike-index/components/ManufacturerDetails.jsx') }
    />
    <Route
      path    = "/manufacturers/:_manufacturerID/bikes/:bikeID/"
      handler = { require('./bike-index/components/BikeDetails.jsx') }
    />

Now BikeDetails will wait for the actions triggered for bikeID and _manufacturerID. Since _manufacturerID doesn't have an refluxActionsForRouterState entry, it will be ignored and we'll only wait for getBike.

appsforartists commented 9 years ago

I thought about my last comment a bit more, and it's flawed in two ways:

  1. This will break your ability to get links to your routes. If you have something like getLink({"manufacturerID": "soma", "bikeID": "juice"}), the _manufacturerID will throw it too.
  2. You may want to trigger some stores but not others. For instance, maybe you have a Manufacturers store that contains the title you want to put in the address bar, but you also have a store that contains all the bikes that manufacturer produces. You'd want getManufacturer to trigger the one that gives you the name to put in the title bar, but not the one that gets the details of that manufacturer.

This will require more thinking, but actionsForRouterState may need refining or replacement.

appsforartists commented 9 years ago

I think I need to add an additional filter for routeName. You can add a routeName to an actionsForRouterState entry and that action will only be triggered if the routeName and the parameterName match.

influx6 commented 9 years ago

So to be exact and accurate the actionsForRouterState is very dynamic that based on route will find and link the necessary stores requested?

gpbl commented 9 years ago

@influx6 yes, basically the router, server-side, must know about actions and stores necessary to render the route handlers and its subcomponents.

I solved this skipping an abstraction level and writing a script for each route. This script 1) collects the data with a set of API requests, and 2) tells which stores should be hydrated with that data. The goal here is to make the 1st point dynamic enough, so we don't need to duplicate such requests, which are already described in actions and store handlers, but hidden between components and routes.

geekyme commented 9 years ago

Would it be possible to seek inspiration from fluxible:

Client - User interactions trigger actions -> actions trigger stores -> stores trigger UI change. Server - Page load trigger actions -> actions trigger stores

Fluxible solved the problem quite effectively by scoping Stores, Actions and Dispatchers to instances with context. However, it involves a great deal of boilerplate and lacks the simplicity and reasonable defaults that Reflux has.

przeor commented 9 years ago

Hi Reactors, are there any new examples of Isomorphic Reflux implementations or this is still long term feature??? Thanks in advance for any guidance :-) as last comment regarding the issue was made few months ago.

przeor commented 9 years ago

FYI: after my research I am heading to www.fluxible.io & https://gitter.im/yahoo/fluxible :-)

bishtawi commented 9 years ago

I didnt see this listed, but two flux libraries that support isomorphism (and bootstrapping the client store from data serialized from the server) are flummox and alt.

EDIT: Never mind about Alt. While you can run it on the server, you have to get the data to populate your stores manually. So its not truly isomorphic.

geekyme commented 9 years ago

And fluxible :)

Sent from my iPhone

On 21 Apr 2015, at 5:41 am, Stephen notifications@github.com wrote:

I didnt see this listed, but two flux libraries that support isomorphism (and bootstrapping the client store from data serialized from the server) are flummox and alt.

— Reply to this email directly or view it on GitHub.

bishtawi commented 9 years ago

In case anyone is looking for a complete isomorphic flux implementation, I ended up going with Marty. I found it to be the most feature complete. Their documentation is very mature as well and they have a nontrivial example app. I was able to get my existing website running isomorphically on Marty with little headache in less than a day.

leebenson commented 9 years ago

Any update on this?

LongLiveCHIEF commented 9 years ago

I'm going to be working on this over the next 3-4 weeks. If anyone wants to help contribute, join the reactiflux slack channel, and keep an eye out for an isomorphic feature branch.

lemonCMS commented 9 years ago

Is there any progress on this front?

spoike commented 9 years ago

We'll be working on context objects in reflux that will contain dehydrate and hydrate functions to handle this. It will show up in 0.2.x branch soon.

andyhite commented 9 years ago

I hate to beat a dead horse, but any update on this?

LongLiveCHIEF commented 9 years ago

We know it won't be coming in the 0.2.x branch at this point. There are a ton of bug fixes within the 0.2.x version to fix first.

leebenson commented 9 years ago

Reflux is excellent and I don't want to detract its use, but if isomorphic is what you need, check out Yahoo's Fluxible

I'm using it in production. It has per-request contexts, isomorphic function calling via Fetchr, and some decent shortcuts for React components to listen to state changes and effect actions.

Coming from Reflux, some users might initially complain of unnecessary boilerplate, but I've not found that to be the case at all. Once it's set-up, it's trivial to build for.

Really liking it.

benbonnet commented 9 years ago

@leebenson what would be the most basic example to date for this implementation ? most starter kits are quite overloaded, they go way further this specific point, so the approach is hard to get

leebenson commented 9 years ago

@bbnnt I agree with you - most examples are quite poor, are out of sync with documentation, or just overloaded to the point that it's not always apparent what's going on.

I figured out my flux stack piece by piece. I had intended to write a minimum scaffold via yeoman, but I just don't have the time... and my own implementation now is so far removed from the examples, that copying and pasting blocks of code wouldn't make much sense.

The best place to start with Fluxible specifically is probably http://fluxible.io/guides/bringing-flux-to-the-server.html - the good stuff doesn't start until 90% down, under "Introducing the Fluxible Library". It gives a very succinct example for executing flux on the server, rendering the result through React, throwing the result back to the user, and actions and stores. Although, I recommend you read the full article so you get an idea how you might so that stuff "manually", and then how Fluxible intends to solve it.

If you follow the example, and examine the source thrown back to the browser, you'll get an example for what's going on.

At this point, the best thing to do is study https://github.com/mridgway/flux-example/tree/master/chat to see how the server and client pieces work. The key concept is rehydration. You want to be able to render and execute the flux flow on the server and the client. The way Fluxible allows for this is to send a fluxible "state" object back with the source code that can be parsed on the client, and pick up where the server left off.

The final piece is Fetchr, which is a Fluxible plug-in. Basically, it's an environment-aware plug-in that allows you to call the same code from the client or the server. If called on the server, it will just execute the function locally (since it's defined locally and available to the server.) If called on the client, it will issue an AJAX request to a server end-point that Fetchr sets up, execute it on the client, and then send the result back as JSON. This is really what makes it "isomorphic".

I'd read that same stuff a dozen times before and it made no sense. It was only following the code examples and trying to re-create it piece by piece that it finally occurred to me how each piece fits together.

Sorry I couldn't be more specific.

kriscarle commented 8 years ago

In case it helps, I got a form of isomorphic "rehydrate" working using reflux in my fork of the reflux-state-mixin (now called Super-Simple-Flux) adding an option to initialize the store using the props of the connected react component. see https://github.com/openmaphub/Super-Simple-Flux/blob/master/src/connectMixin.js

The basic idea is that the Mixin sits in between the store and Reflux.connect() and provides an opportunity to override getInitialState() before the state is seen by any of the listeners. No more content flashing off and back on again...

My isomorphic implementation already syncs over props from the server to the client for the top-level component, and that usually is the same data I need for initialState so this works well. Super-Simple-Flux is also a great way to avoid all the boilerplate to keep your component state in sync with the store. It just extends the component's state with the store state. https://github.com/yonatanmn/Super-Simple-Flux.

I also plan to implement a similar feature to restore state from browser local storage for offline/auto-save support.

spoike commented 8 years ago

Cool :+1: Should probably add that to the Readme file.