Skip to content

Instantly share code, notes, and snippets.

@gaearon
Created June 3, 2015 18:03
Show Gist options
  • Star 123 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • 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!
@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