Closed ccorcos closed 9 years ago
redux-form is a great tool to use to handle forms.
Reducers can pass to other reducers. The basic idea here is to create a high-order reducer that delegates down to further reducers for each group of inputs you have. Those reducers could act on generic actions such as ON_CHANGE
. Remember, you can include whatever data you would like in an action, so it's not necessary to have a reducer for each field in a group, just include that data in the action.
A very striped back version of this could be:
inputReducer = function(state={}, action) {
if (action.type === 'ON_CHANGE') {
return {
...state,
[action.field]: action.value
};
}
return state;
}
formReducer = function(state={}, action) {
const { form, ...rest } = action;
if (form) {
return {
...state,
[form]: inputReducer(state[form], rest)
};
}
return state
}
inputOnChangeAction = function(form, field, value) {
return {
type: 'ON_CHANGE', form, field, value
};
}
I see. That nice little snippet. I'm going to use that.
I'm still just a little confused though. One of the things I like to consider when building a web application is if I can have two versions of app running side by side in different divs. So long as there isnt user auth and browser cookies that are inherently global, then a well-written application (without globals) should be able to do this.
The problem I'm having with Redux when approaching this problem is suppose I build a todo's app. The entire todo's app is done. Ok, now I want to have two todos apps side-by-side. It should be really easy, right? Well it seems I'll have to change all of my reducers to be aware of the fact that there are multiple todos apps. Ideally, there would be some way of contextualizing each todo's app so they wouldnt have to be aware of where they are in the grand scheme of things.
Ideally you could do something like this:
App = React.createClass({
render: function() {
return (
<div>
<TodosApp id=1/>
<TodosApp id=2/>
</div>
)
}
})
And the id in there basically "lifts" all the actions, reducers, and state to reflect that...
I guess what I'm thinking is you'd have a store paired with each component. In this case, you'd have store for each TodosApp. Then hopefully theres some way of combining stores... but I'm not sure thats possible...
You could that. There's only ever one store in redux, just compose your app's main reducer in a higher reducer.
appsReducer = (state={}, action) => {
const { app, ...rest } = action;
if (app) {
return {
...state,
[app]: appReducer(state[app], rest)
};
}
return state;
};
Then bind the id prop from TodosApp
to any actions it dispatches as app: id
Hmm. I was under the impression that the actions should only be discriminated only by the "type" property. At least thats what it seems like. The Redux dev tool looks for the type, right?
What if the type was an array and the reducer was concerned only with the head of the type array. That way, it would be easier to abstract. Here's a solid example -- I'm very curious what you think...
We have a simple input action/reducer pair that concerns itself with a single input.
inputReducer = function(state='', action) {
if (action.type[0] === 'ON_CHANGE') {
return action.value
}
return state
}
inputOnChangeAction = function(e) {
return {
type: ['ON_CHANGE'],
value: e.target.value
}
}
The goal is to reuse those actions and reducers to abstract up to a form component like this:
Form = React.createClass({
render: function() {
return (
<input value={this.props.username} onChange={this.props.usernameOnChange}/>
<input value={this.props.password} onChange={this.props.passwordOnChange}/>
)
}
})
So what if we has these high-order functions to lift the actions and reducers by adding a new type to the beginning of the type array.
liftAction = function(type, f) {
return function(arg) {
let action = f(arg)
action.type = [type, ...action.type]
return action
}
}
liftReducer = function(type, f) {
return function(state, action) {
if (action.type[0] === type) {
return f(state, action.splice(1))
} else {
return state
}
}
}
This the top-level actions and reducers work like this:
reducer = function(state={username:'', password:''}, action) {
return {
username: liftReducer('username', inputReducer)
password: liftReducer('password', inputReducer)
}
}
actions = {
usernameOnChange: liftAction('username', inputOnChangeAction)
passwordOnChange: liftAction('password', inputOnChangeAction)
}
Does that make sense?
Thus for the side-by-side todo's example, we'd basically just do this:
reducer = function(state, action) {
return {
todos1: liftReducer('todos1', todosReducer)
todos2: liftReducer('todos1', todosReducer)
}
}
// liftActions just maps liftAction over the object values...
todos1actions = liftActions('todos1', todosActions)
todos2actions = liftActions('todos2', todosActions)
Does that make sense? Is this formalized in any way?
Almost, though instead of adding anything to the type, you can just add it to the action itself.
Again, refering to redux-form
, here's an example helper that is similar to your liftAction
but instead of adding to the type, it just adds a field to the action: https://github.com/erikras/redux-form/blob/master/src/bindActionData.js
Therefor, you wouldn't need to a liftReducer
, you would just have a top-level reducer that takes those added fields to an action and delegates the rest of the action to the proper area in the state tree.
That being said, what's great about Redux is that it is as un-opinionated as it gets, which means if there is a viable alternative, by all means go for it, I'm just pointing out how these problems have already been solved for reference.
I think this is pretty much the same as https://github.com/rackt/redux/issues/822. You can get full reusability with more strict Elm-like architecture, but this is incompatible with middleware so you need to choose which is more important to you.
@Nicktho thanks for the info. redux-form is interesting, but I'm still learning Redux so that redux-form is too much of a black box for me right now...
@gaearon thats exactly what I was looking for -- that's the essence of the question I'm asking here.
Relevant new discussion: https://github.com/reactjs/redux/issues/1528
its been a while, but I just had an idea and put together a little demo: https://github.com/ccorcos/reduxish
Its basically adopting a the elm 0.16 architecture to redux.
I'm just getting started with redux and it looks amazing, but I'm a little worried about abstraction.
Suppose I have a really simple input component
Now its trivial to hook this up to a component with an input.
But what happens when I have more than one input?
Ok, I suppose we could change the action type to specify which input its referring too.
But now suppose I place two of the same views side-by-side? Now I need to modify the action yet again to specify which input in which view I'm referring to.
Oy vey. Whats tough about this is that the input reducer somehow needs to know about how the rest of the app is structured. That just doesnt seem right. Ideally we would be able to abstract out the input's actions and reducers based on how the parent decides to arrange them. I suppose we could do this with a high-order function. But this is all getting pretty tedious. Am i missing something? How are you dealing with this?