-
-
Save gaearon/d77ca812015c0356654f to your computer and use it in GitHub Desktop.
// ------------ | |
// 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! |
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})
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:
@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
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.
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:
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.