Skip to content

Instantly share code, notes, and snippets.

@gaearon
Created June 3, 2015 18:03
Show Gist options
  • Save gaearon/d77ca812015c0356654f to your computer and use it in GitHub Desktop.
Save gaearon/d77ca812015c0356654f to your computer and use it in GitHub Desktop.
Combining Stateless Stores
// ------------
// counterStore.js
// ------------
import {
INCREMENT_COUNTER,
DECREMENT_COUNTER
} from '../constants/ActionTypes';
const initialState = { counter: 0 };
function increment({ counter }) {
return { counter: counter + 1 };
}
function decrement({ counter }) {
return { counter: counter - 1 };
}
export default function counterStore(state = initialState, action) {
switch (action.type) {
case INCREMENT_COUNTER:
return increment(state, action);
case DECREMENT_COUNTER:
return decrement(state, action);
default:
return state;
}
}
// ------------
// todoStore.js
// ------------
import { ADD_TODO } from '../constants/ActionTypes';
const initialState = {
todos: [{
text: 'do something',
id: 0
}]
};
export default function todoStore(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return {
todos: [{
id: state.todos[0].id + 1,
text: action.text
}].concat(state.todos)
};
}
return state;
}
// ------------
// combinedStore.js
// ------------
// Let's say at some point I know that these stores depend on each other in some way.
// If I *decide* I want to hide these stores as impl details of a single store
// I don't need to change their public APIs at all. I just register combinedStore instead.
import counterStore from './counterStore';
import todoStore from '../todoStore';
const initialState = {
counterData: undefined,
todoData: undefined
};
export default function combinedStore(state = initialState, action) {
const counterData = counterStore(state.counterData, action);
const todoData = todoStore(state.todoData, action);
return { counterData, todoData };
}
// So it's trivial to "merge" stores but keep the delegation. This is exactly how Elm models work too.
// Now, if I *want* to, I can make substores more custom (e.g. make a store factory that responds only to
// actions matching predicate, like createFollowersStore(userId) => FollowersStore that responds to specific
// userId in the action). Composition all the way!
@fisherwebdev
Copy link

Let's say counterStore increments/decrements based on ADD_TODO / DELETE_TODO, only when the todo is actually added to cache. todoStore takes care of ensuring there are no repeated todos. (this is perhaps a lame hypothetical example, but please let's just entertain this for a bit.)

This would require counterStore to respond to the action after todoStore, reading from todoStore and checking that the new todo was actually added.

How do we ensure the order of operations without waitFor()?

We could check if the new todo is within the cache elsewhere -- that is move the deduping logic out of the store -- but isn't this moving application logic out of the store and into a place where it really doesn't belong?

@gaearon
Copy link
Author

gaearon commented Jun 3, 2015

In this case counterStore no longer truly depends on the state and action. It also depends on hasTodoReallyBeenAdded.

We add this parameter to its API. Sure, it's no longer usable “as is”, but that's the point: it has an explicit dependency now on other data. It's not a top-level store. Whoever manages it must somehow give it that data.

export default function counterStore(state = initialState, action, hasTodoReallyBeenAdded) {

Now, todoStore must answer the question of whether a TODO was actually added. If this answer is worth writing into state, nothing changes inside it. If not, we should change its return value to { state, hasTodoReallyBeenAdded } to reflect this. Again, it's now not usable as a Store directly, but the additional data it computes is now made clear.

  return { hasTodoReallyBeenAdded, state };
}

Finally, the combinedStore is changed to look like this:

export default function combinedStore(state = initialState, action) {
  const { state: todoData, hasTodoReallyBeenAdded } = todoStore(state.todoData, action);
  const counterData  = counterStore(state.counterData, action, hasTodoReallyBeenAdded);
  return { counterData, todoData };
}

All data dependencies are explicit. The order is determined by the order of the calls—because you do them.

@fisherwebdev
Copy link

Do we not register the two substores and we only register the combined store?

And then it's a synchronous invocation of the store-functions within the combined store, with ordering reversed from what you have in the original gist.

Sorry if I'm slow on this one. Really great use of composition!

@alexeyraspopov
Copy link

return { hasTodoReallyBeenAdded, state };

It looks like a hack. In this case, somewhere in components we need to know that todoStore holds not only his state.

And from another perspective: what if I only want to use counterStore? Why should I rely on some combinedStore? It's not clear dependency.

@gaearon
Copy link
Author

gaearon commented Jun 3, 2015

Do we not register the two substores and we only register the combined store?

Yeah. Sorry if I haven't been clear. My example was to show how easy it is to refactor "two registered top-level stores" into "one top-level store that reuses previously independent stores' code" with this approach. Because stores are just functions, they compose perfectly. As soon as you need to introduce a dependency between a few stores, you can create a “parent” store that uses them as is in a matter of minutes, and then figure out how their (now internal) API needs to change.

Once they're no longer top-level stores, their function APIs no longer have to conform to (state, action) => state and you can customize them further if needed. For example, you can extract and reuse helpers for pagination that are used by multiple stores.

And then it's a synchronous invocation of the store-functions within the combined store, with ordering reversed from what you have above.

Why reversed? You said first TodoStore confirms, then CounterStore increment. So that's why I let Todo “substore” handle first, then use pass its response to the CounterStore, then return the “combined” result.

@trabianmatt
Copy link

Couldn't the combinedStore handle the logic of whether to update the counterStore by explicitly switching on those actions?

export default function combinedStore(state = initialState, action) {

  switch (action.type) {
  case ADD_TODO:
  case DELETE_TODO:

    const todoData = todoStore(state.todoData, action);

    if (todoAddedSuccessfully(todoData, action.text)) {
      const counterData = counterStore(store.counterData, action);
      return { counterData, todoData };
    }

    return { counterData: state.counterData, todoData };

  default:
    const counterData = counterStore(state.counterData, action);
    const todoData = todoStore(state.todoData, action);
    return { counterData, todoData };
  }

}

@gaearon
Copy link
Author

gaearon commented Jun 3, 2015

And from another perspective: what if I only want to use counterStore? Why should I rely on some combinedStore? It's not clear dependency.

Sorry, I don't understand the question. Say you realized CounterStore and TodoStore should've been a single store because they have internal dependencies. This is the case I'm considering in the gist, showing how easy it is to accomplish with functional Stores.

It looks like a hack. In this case, somewhere in components we need to know that todoStore holds not only his state.

Why? I don't understand. The only change in components is to subscribe to the new combined store instead. Wherever you read the data in components, you'll need to grab a level deeper instead.

@gaearon
Copy link
Author

gaearon commented Jun 3, 2015

Couldn't the combinedStore handle the logic of whether to update the counterStore by explicitly switching on those actions?

Sure, it's another option. Basically since those a functions, it's up to you to declare the contract between them. Because they don't have top-level mutable state, the pieces are easy to move around, just like React components.

@alexeyraspopov
Copy link

The only change in components is to subscribe to the new combined store instead.

Okay, now it makes sense, thanks.

@fisherwebdev
Copy link

Might be worth mentioning that if you need a different dependency ordering per action, the combinedStore can include logic to manage that.

If we play this out with composition-upon-composition into a deep dependency tree, I think we could wind up in a place where we might have only one root store registered with the dispatcher and all special cases of dependency ordering declared as composed stores invoked by the root store. But in practice, really, most of the stores won't be involved in dependencies and we won't have a need to include them in that dependency composition tree.

@goatslacker
Copy link

I think the stores as functions mainly buys you these two things:

  • Stores and actions are just pure functions. They are easily testable in isolation.
  • No need for waitFor().

The rest you can have with any other implementation.

The waitFor one is mostly a syntax change:

function CombinedStore(state = initialState) {
  this.state = state
}

CombinedStore.prototype.reduce = function (state, action) {
  this.waitFor(CounterStore, TodoStore);
  const counterData = CounterStore.getData(state.counterData, action);
  const todoData = TodoStore.getTodo(state.todoData, action);
  return { counterData, todoData };
}

but a very nice change. I really like the just functions approach.

@gaearon
Copy link
Author

gaearon commented Jun 3, 2015

If we play this out with composition-upon-composition into a deep dependency tree, I think we could wind up in a place where we might have only one root store registered with the dispatcher and all special cases of dependency ordering declared as composed stores invoked by the root store. But in practice, really, most of the stores won't be involved in dependencies and we won't have a need to include them in that dependency composition tree.

Precisely. One Store to rule them might sound alluring but in reality IMO it's easier if the library hides this from you—but lets you make a tree if you know what you're doing.

@tomkis
Copy link

tomkis commented Jun 4, 2015

You might want to revive this

Anyway, I would say that the need to use waitFor to keep particular order of handled actions is most likely caused by wrong architectural decision. Stateful store is supposed to encapsulate specific domain and the state is responsible to determine those boundaries. However, if the store is stateless and entire state of app is held in single tree, the decision about store boundaries is not that strict and we create those boundaries only to make our code logically grouped together (we could possibly have just single store to handle all actions, which is of course not ideal). The tree itself is responsible for emitting change for specific views which really need to update. So in other words, if we really need to rely on order then it's probably better to handle everything in single action handler, but in most cases we could avoid it by proper schema of the tree.

@jordangarcia
Copy link

By separating the concern of how your application state handles actions (the writes) and how to read your application state then store boundaries become a lot less important.

Ultimately it's the ability to compose your application state and view it through any lens that makes a singular tree-like app state very powerful.

This was a very conscious design decision of NuclearJS. By having the overarching framework be responsible for dispatching actions and notification it allows all observers to get a singular immutable snapshot of the world every dispatch loop.

The hard part is performance. By having everything in a singular app state map or store then using a store boundary as the unit of measurable change doesn't work anymore. In NuclearJS we chose to use ImmutableJS, which made deep quality checks in a map relatively free at the expense of enforcing ImmutableJS throughout the stack (not necessarily a bad thing)

Am very interested to see how this library evolves!

@leoasis
Copy link

leoasis commented Jun 4, 2015

This looks awesome because it resembles more and more to the architecture exposed in Elm (https://github.com/evancz/elm-architecture-tutorial) sans the signals concept (which isn't a lot to grasp anyway). I love it because of its simplicity, and flexibility to acommodate different state requirements. It's just functions and composition, as simple as it gets.

@matystl
Copy link

matystl commented Jun 5, 2015

As i see it you have two type of dependencies for data. One is store dependency on data a and second one is derivated data.
For first problem my ideas is this:
Lets see stores as "entities" which provide initial states and some reducing functions over this state. Combining these two into one function which react on action and decide which reducing function should be run and you get exactly this lib. If your application is fair simple than this might be enough for you. And what for more complication situation? What about two stores depend on each other? Easy way to grasp this is to abandon idea of simple dispatching in original flux. What if we take building blocks of stores in this lib(initial states and reducers) and remake dispatching. For every action there will be one place(one meta-store or you can split it between multiple stores-stores) which will be responsible for calling reducers over corresponding parts of state. Then you can precisely control what reducers/in what order and with what data will be called. This will also add you ability to pass reducers not only state from one store but for multiple stores. For keeping this design sane reducer can modify only state in their responsibility like now but can read(depend) on multiple stores data. You will not need waitFor because you will have explicit dependencies stated in this meta-store.

On derivated data story. If you have DRY state than this state is usually not best for ui. Derivated data in their essence are pure functions over multiple store data which returns this data combined somehow. So without any further attempt in component you can subscribe to multiple stores and feed their values into this pure function and use result of this in rendering. This is not enough if you want to reuse this in more than one component which are not is child-parent relationship(can be passed as props) or if you don't want to expose this dependency between stores inside rendering and have it outside. With little bit of effort this abstraction can be implemented that derivated data for components will look like store and components can reed them and listen on it. From dispacher point of view after he run action through stores he will recalculate derivated data and only after that will issue change events to component. For performace reason you can use imutable data and caching results of derivated functions.

@slorber
Copy link

slorber commented Jun 10, 2015

Hi,

Dan your idea looks similar to what I use in https://github.com/stample/atom-react except we have different API's

For me a store is just an element that project events to a state

I don't have yet this API but will tend to evolve to something akin to this:

var todoStore = function(cursor,event) {
}
var counterStore = function(cursor,event) {
}

var  someComposedStore = function(cursor,event) {
  todoStore(cursor.follow("todos"),event);
  counterStore(cursor.follow("counter"),event);

  // Custom code can be plugged here, by using the content of the other stores
  var todoNumber = cursor.follow("counter").get();
  cursor.follow("moreThan10").set(todoNumber > 10);
}

var  rootStore = function(cursor,event) {
  someComposedStore(cursor.follow("someComposedStore"),event);
}

eventStream.wireTo(rootStore)

Being able to compose stores inside another store permits to remove the store dependencies with waitFor, without introducing too much code duplication.
It generated some extra storage and immutable data copying but it has not been a problem for me until now.

@speedskater
Copy link

Hi,

I like your idea of redux and composing stores but I see the same problem as @alexeyraspopov mentioned regarding combininig independent stores. The reason is, that the container component must know the composition hierarchy. Therefore previously independent store/component combinations would be coupled with other stores.

Therefore I would propose to provide an alternative way to combine stores in a flat way.

The combining function would look like this:

export default function combinedStore({counter, todos} = initialState, action) {
  ({ counter } = counterStore({counter: counter}, action));
  ({ todos } = todoStore({todos: todos}, action));
  return { counter, todos };
}

The composeStoresFlat would like like this:

function composeStoresFlat(...stores) {
    let storeMapping = new Map();

    let internalStores = stores.map(store => {
        let initialState = store();
        let keysForStore = Object.keys(initialState);

        keysForStore.forEach(key => {
            if (storeMapping.has(key)) {
                throw new Error("Two stores provide state for the same identifier: " + key);
            } else {
                storeMapping.set(key, store);
            }
        });


        return (state, action) => {
            return _.pick(store(_.pick(state, keysForStore), action), keysForStore);
        }
    });

    return (state, action) => {
        let newState = {};
        internalStores.forEach(store => {
            Object.assign(store(state, action));
        });
        return newState;
    }
};

The initialization of redux would be done in the following way:

const dispatcher = createDispatcher(
    composeStoresFlat(combinedStore, anotherStore),
    getState => [thunkMiddleware(getState)] // Pass the default middleware
);

const redux = createRedux(dispatcher);

Finally the component needing the counter information can select the counter independent of the todos and vice versa. Hence it allows to reuse stores and corresponding container components independent of their composition:

state => ({ todos: state.todos })
counter => ({ counter: state.counter})

@gaearon
Copy link
Author

gaearon commented Jun 22, 2015

@speedskater

I understand what you're suggesting but I'm sure it's going to be a pain in a large app to keep inventing keys so they don't clash, and it will look exactly the same namespaced way in the end.

I think that the problem of components tied to particular state keys is better solved by composing their select methods. This is exactly what NuclearJS does with "getters". It's already possible in Redux but not really documented yet. Here's some info:

reduxjs/redux#160 (comment)
reduxjs/redux#47

@quirinpa
Copy link

@fisherwebdev I just published a small middleware that i think can help you solve that problem: redux-next I hope it helps but it doesn't quite react to the store though... :P

Here are a cople of other middlewares i made redux-delay and redux-client-next (middleware creator function) lol @ my useless contributions

@goldensunliu
Copy link

I can see the benefit of combined store that rules them all so stores don't have to talk/depend on each other, however i.e the combined store now owns the dependency as one abstraction level up, this is more likely to be more readable and easier to manage as an application grows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment