// ------------ | |
// 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! |
This comment has been minimized.
This comment has been minimized.
In this case 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, return { hasTodoReallyBeenAdded, state };
} Finally, the 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. |
This comment has been minimized.
This comment has been minimized.
fisherwebdev
commented
Jun 3, 2015
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! |
This comment has been minimized.
This comment has been minimized.
alexeyraspopov
commented
Jun 3, 2015
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. |
This comment has been minimized.
This comment has been minimized.
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
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. |
This comment has been minimized.
This comment has been minimized.
trabianmatt
commented
Jun 3, 2015
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 };
}
} |
This comment has been minimized.
This comment has been minimized.
Sorry, I don't understand the question. Say you realized
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
alexeyraspopov
commented
Jun 3, 2015
Okay, now it makes sense, thanks. |
This comment has been minimized.
This comment has been minimized.
fisherwebdev
commented
Jun 3, 2015
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. |
This comment has been minimized.
This comment has been minimized.
goatslacker
commented
Jun 3, 2015
I think the stores as functions mainly buys you these two things:
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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
tomkis
commented
Jun 4, 2015
You might want to revive this
|
This comment has been minimized.
This comment has been minimized.
jordangarcia
commented
Jun 4, 2015
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! |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
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. 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. |
This comment has been minimized.
This comment has been minimized.
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. |
This comment has been minimized.
This comment has been minimized.
speedskater
commented
Jun 17, 2015
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}) |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
quirinpa
commented
Jul 30, 2015
@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 |
This comment has been minimized.
This comment has been minimized.
goldensunliu
commented
Aug 14, 2015
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. |
This comment has been minimized.
fisherwebdev commentedJun 3, 2015
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?