Skip to content
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

how to compose redux reducers with the same actions? #897

Closed
ccorcos opened this issue Oct 14, 2015 · 11 comments
Closed

how to compose redux reducers with the same actions? #897

ccorcos opened this issue Oct 14, 2015 · 11 comments

Comments

@ccorcos
Copy link

ccorcos commented Oct 14, 2015

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

inputReducer = function(state='', action) {
  if (action.type === 'ON_CHANGE') {
    return action.value
  }
  return state
}

inputOnChangeAction = function(e) {
  return {
    type: 'ON_CHANGE',
    value: e.target.value
  }
}

Now its trivial to hook this up to a component with an input.

reducer = combineReducer({
  username: inputReducer
})

But what happens when I have more than one input?

reducer = combineReducer({
  username: inputReducer
  password: inputReducer
})

Ok, I suppose we could change the action type to specify which input its referring too.

inputReducer = name => (state='', action) => {
  if (action.type === 'ON_CHANGE_'+name.toUpperCase()) {
    return action.value
  }
  return state
}

inputOnChangeAction = name => (e) => {
  return {
    type: 'ON_CHANGE_'+name.toUpperCase(),
    value: e.target.value
  }
}

reducer = combineReducer({
  username: inputReducer('username')
  password: inputReducer('password')
})

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.

inputReducer = index => name => (state='', action) => {
  if (action.type === 'ON_CHANGE_'+name.toUpperCase()+'['+index+']') {
    return action.value
  }
  return state
}

inputOnChangeAction = index => name => (e) => {
  return {
    type: 'ON_CHANGE_'+name.toUpperCase()+'['+index+']',
    value: e.target.value
  }
}

reducer = function(state:[{username:'', password:''},{username:'', password:''}], action) {
  return state.map({username, password}, index) => {
    return {
      username: inputReducer(index)('username')(username)
      password: inputReducer(index)('password')(password)
    }
  }
}

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?

@Nicktho
Copy link

Nicktho commented Oct 14, 2015

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
  };
}

@ccorcos
Copy link
Author

ccorcos commented Oct 14, 2015

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.

@ccorcos
Copy link
Author

ccorcos commented Oct 14, 2015

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...

@ccorcos
Copy link
Author

ccorcos commented Oct 14, 2015

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...

@Nicktho
Copy link

Nicktho commented Oct 14, 2015

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

@ccorcos
Copy link
Author

ccorcos commented Oct 14, 2015

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?

@Nicktho
Copy link

Nicktho commented Oct 14, 2015

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.

@gaearon
Copy link
Contributor

gaearon commented Oct 14, 2015

I think this is pretty much the same as #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.

@ccorcos
Copy link
Author

ccorcos commented Oct 15, 2015

@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.

@gaearon
Copy link
Contributor

gaearon commented Mar 18, 2016

Relevant new discussion: #1528

dustinmoorenet added a commit to dustinmoorenet/redux that referenced this issue May 6, 2016
```javascript
function firstName(state = '', action = {}) {
  if (action.type === SET_FIRST_NAME) {
    return action.payload
  }

  return state
}

function lastName(state = '', action = {}) {
  if (action.type === SET_LAST_NAME) {
    return action.payload
  }

  return state
}

const reducer = combineReducers({
  firstName,
  lastName
})

function handleSetFullName(state, action) {
  if (action.type === SET_FULL_NAME) {
    const parts = action.payload.split(' ')

    return {
      ...state,
      firstName: parts[0],
      lastName: parts[1]
    }
  }

  return state
}

export default composeReducers(
  reducer,
  handleSetFullName
);
```

reduxjs#897
dustinmoorenet added a commit to dustinmoorenet/redux that referenced this issue May 6, 2016
```
function firstName(state = '', action = {}) {
  if (action.type === SET_FIRST_NAME) {
    return action.payload
  }

  return state
}

function lastName(state = '', action = {}) {
  if (action.type === SET_LAST_NAME) {
    return action.payload
  }

  return state
}

const reducer = combineReducers({
  firstName,
  lastName
})

function handleSetFullName(state, action) {
  if (action.type === SET_FULL_NAME) {
    const parts = action.payload.split(' ')

    return {
      ...state,
      firstName: parts[0],
      lastName: parts[1]
    }
  }

  return state
}

export default composeReducers(
  reducer,
  handleSetFullName
);
```

reduxjs#897
@ccorcos
Copy link
Author

ccorcos commented Nov 30, 2016

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants