-
Notifications
You must be signed in to change notification settings - Fork 297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Additional arguments for handleAction{,s} reducers #148
Comments
I'm not sure if we should be passing any arguments other Couple of questions.
|
I actually followed the advice given by @gaearon in reduxjs/redux#749 (comment). One thing about the arguments splat is that it won't have an effect unless you're actually invoking the reduced reducer from Sorry about the example using Immutable, I get that it may clutter up the use case illustration a bit if you're not familiar with its api. Here's the example from @gaearon's comment reposted: function a(state, action) { }
function b(state, action, a) { } // depends on a's state
function something(state = {}, action) {
let a = a(state.a, action);
let b = b(state.b, action, a); // note: b depends on a for computation
return { a, b };
} This is sort of a minimal example not tied to a specific use case. The log reducer is already "higher" up in the reducer chain, although it only receives it's own namespace so it can't fiddle with the global state (using Immutable you can actually enforce this, not just encourage it). It could get passed the entire state, but that doesn't solve the need for knowing both the before and after state once the other reducers have run. A perhaps better (although contrived) example: // Standard self-contained reducers
const adders = combineReducers({
foo: handleAction(fooAdd, (state = 0, action) => state + action.payload),
bar: handleAction(barAdd, (state = 0, action) => state + action.payload),
});
// "Monitoring" reducer, reacting to other updates to the state
const log = handleActions({
[clearLog](state, action) {
return [];
},
[fooAdd](state, action, prev, next) {
const updates = [];
if (next.foo === prev.foo) {
updates.push('foo is unchanged');
} else if (next.foo > next.bar) {
updates.push('foo is greater than bar');
}
return state.concat(updates);
},
[barAdd](state, action, prev, next) {
const updates = [];
if (next.bar === prev.bar) {
updates.push('bar is unchanged');
} else if (next.bar > next.foo) {
updates.push('bar is greater than foo');
}
return state.concat(updates);
}
}, []);
function root(state = {}, action) {
// foo and bar do their thing
const next = adders(state, action);
// log depend on state updated by firstStage reducers
return log(next.log, action, state, next);
} The logging reducer is just one application of this kind of use case. Another example could be something like a "Game over" monitor, which checks that all enemies are dead, the player's health is depleted or time has run out. It could be very clean to model this in the way that regular reducers run their course, then after the tick, the monitor reducer runs and determines if we're at end of game or not. Middlewares are also a powerful way to implement cross-cutting concerns. They can easily inspect the state before and after core reducers have run. However, I sincerely feel like these kind of operations are in fact reducers. They're deterministic and pure thus fulfilling reducer requirements in every other respect. |
Thanks for the clarification; it's great to learn about this use case. function root(state = {}, action) {
// foo and bar do their thing
const next = adders(state, action);
// log depend on state updated by firstStage reducers
return log(next.log, action, state, next);
} Where does I've seen the thread reduxjs/redux#749 before and am interested in reading through it in more detail. What do you think about submitting a PR for this? Also, what would the middleware implementation look like? |
I could have a look at a PR, though not right away. As I noted in the first comment, there needs to be an update to I don't have time to sketch out a middleware example in code right now. But conceptually you'd have to inspect all dispatched actions. If the action is one that might trigger new log output ( However, both of those options are suboptimal though. |
I have a few concerns with allowing additional arguments: We are moving away from the original concept of a reducer ´(state, action) => state` which is super simple and beginner-friendly. Additional arguments can be confusing and rely heavily on the root reducer/other dependencies. The game over example is imo a perfect example of using a (memoized) selector. This is nothing that has to be a reducer. The container component, e.g.
export default handleAction(fooAdd, (state = 0, action) => state + action.payload)
export default handleAction(barAdd, (state = 0, action) => state + action.payload)
// ...
function GameOver(props) {
if (props.gameOver) {
return // Presentational Component
}
return null
}
function mapStateToProps(state, ownProps) {
const { foo, bar } = state
return {
gameOver: foo < bar
}
}
export default connect(mapStateToProps)(GameOver) We should stick to the principle of unidirectional data flow. The log reducer can be computed by selecting from the state too, it's nothing that has to be an additional reducer imo. The
export default combineReducers({
// Use an array instead for holding previous and current value.
foo: handleAction(fooAdd, (state = [0, 0], action) => state.shift().concat(state[1] + action.payload)),
bar: handleAction(fooAdd, (state = [0, 0], action) => state.shift().concat(state[1] + action.payload))
})
// ...
class Log extends React.Component {
constructor(props) {
super(props)
this.state = {
log: []
}
}
onComponentWillReceiveProps(nextProps) {
this.setState({
log: [...this.state.log, nextProps.result]
})
}
render() {
// Render `this.state.log`
}
}
// Selector
function getResult(state) {
const { foo, bar } = state.adders;
const result = [];
if (foo[0] === foo[1]) {
result.push('foo is unchanged')
}
if (bar[0] === bar[1]) {
result.push('bar is unchanged')
}
if (foo[1] > bar[1]) {
result.push('foo is greater than bar')
} else if (bar[1] > foo[1]) {
result.push('bar is greater than foo')
}
return result;
}
function mapStateToProps(state, ownProps) {
return {
result: getResult(state)
}
}
export default connect(mapStateToProps)(Log) |
I am not sure if this will cause any confusing. Additional arguments is optional, beginners doesn't need to know their existence. Personally I found several case will require additional arguments.
const rootState = {
ui: {inputColor: 'red'},
entities: {
messages: {}, emails: {}, chats: {},
}
};
const getAdditionArgs = (state) => {
return {inputColor: state.ui.inputColor};
}
const rootReducer = (state, action) => {
return {
ui: uiReducer(state, action),
// add extra argument here won't break anything in large project
// and only need change several reducers which has actual business logic.
entities: entitiesReducer(state, action, getAdditionArgs(state)),
}
}; Another option is add getAdditionArgs in action payload.
|
Now that you mention it @timche, I totally agree that the "game over" concept is best modelled with a selector, as you say. For the log concept, it's possible to do as you suggest, basically keeping track of all actions going through the reducer. What I don't like about that though is the fact that the logic behind the reducer is reduced (no pun intended) to basically a simple I do see the argument for beginner-friendly-ness, yet it wouldn't directly be conflicting with the canonical reducer signature. Additional arguments is purely optional after all. Also, it doesn't have to be verbosely advocated in the documentation, but would definitely deserve a place in an "Advanced use" section. This feature is not something I'm going to keep on insisting you add though. I do sincerely respect your reservations against it. |
@myme @xiaohanzhang @yangmillstheory I'm sorry for the slow progress here. If we see that this feature is "largely" requested, I'm convinced to add it. As a library maintainer I think it's important to not add every single feature request. The goal of this library should be to provide a stable and simple API and user-friendly developer experience. |
I agree @timche. I would say it's a convenient feature (purely subjectively), and wouldn't necessary compromise compatibility. However, that doesn't mean that it's correct to add it simply because you can. Furthermore, I don't think there's any point in leaving this open to clutter up the bug list if there's no incentive to begin working on it. |
Just wanted to contribute for this issue. I think this change is good. I realize it violates reducer signature if the users use the additional arguments, but from what I can gather, this is a great way of passing global state as a read only to a slice reducer that can write to it's own slice. The pattern seems fine and the change seems harmless, since it would change literally anything, it'll just be less opinionated on how you use yours reducers which are, as we know, +1 for the change |
+1 for the change; I am now contemplating forking redux-actions -- which I've been using for years -- just to add that one capability. Applying selectors to the entire input state of the composite reducer in deep slice reducers looks like the cleanest solution for the cross-cutting concern problem. |
+1 for the change |
A feature request here.
I have a reducer which have cross-cutting concerns, basically an action logger which puts log lines in the store based on different kinds of actions. This logger needs to know about a broader part of the state than the list of log statements its reducing. By sequencing the reducers in the root reducer, the log reducer can get for example the previous and next version of the state and log accordingly.
Having a root reducer similar to the following (using Immutable containers):
I'd optimally want to do the following:
But since handleActions does not relay the extra arguments, I'm forced to generate the reducers each time and close around the extra arguments like so:
This is both extra verbose and inefficient. It would be great if both
handleAction
andhandleActions
returns reducers that fetch a rest spread and passes it on. I guessreduce-reducers
needs to know about this as well.Something like the following would probably do:
The text was updated successfully, but these errors were encountered: