Skip to content

Instantly share code, notes, and snippets.

@gastonmorixe
Last active September 18, 2018 11:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gastonmorixe/cf6f40578524ddd085dd to your computer and use it in GitHub Desktop.
Save gastonmorixe/cf6f40578524ddd085dd to your computer and use it in GitHub Desktop.
Redux React CombineReducers Discussion [With Dan Abramov]
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import { invites } from './Invites'
import { claimInvite } from './ClaimInvite'
import { app } from './App'
import * as actions from '../constants/ActionTypes'
import {mapping, parseRemotePersonJSON} from './App'
const combinedReducer = combineReducers({
invites,
app,
claimInvite,
})
const rootAccessReducer = (state, action) => {
switch (action.type) {
case `${actions.REQUEST_INVITE}_FULFILLED`:
return { ...state,
app:{...state.app,
loggedPerson: {...state.app.loggedPerson,
...parseRemotePersonJSON(action.payload.person),
authToken: state.app.loggedPerson.authToken
}
},
claimInvite:{...state.claimInvite,
inviteRequest: {...state.claimInvite.inviteRequest,
state: 'loaded'
}
}
}
default:
return state
}
}
const rootReducer = reduceReducers(combinedReducer, rootAccessReducer)
export default rootReducer
@gastonmorixe
Copy link
Author

  1. User requests an invite (Async action will throw 3 actions back, pending, fulfilled, failed)
  2. Invites reducer (combined) should take care of this 3 actions.
  3. The invite request m on successs returns a "person" model serialized.

Problem.

  1. We store the person data under the app reducer
  2. If the fullfilled action is managed in the invites reducer, I can not access the app scope to update the data of the logged person.

@gaearon
Copy link

gaearon commented Dec 23, 2015

Why don't both claimInvite and app react to ``${actions.REQUEST_INVITE}_FULFILLED` independently?
Many reducers can handle many actions, it doesn't have to be a 1:1 mapping.

@gaearon
Copy link

gaearon commented Dec 23, 2015

So basically instead of doing it separately I'd expect ClaimInvite.js to look like

const inviteRequest = (state = { state: 'empty' }, action) {
  switch (action.type) {
    case  `${actions.REQUEST_INVITE}_FULFILLED`:
      return { state: 'loaded' };
    // ...other cases...
    default:
      return state;
  }
};

export const claimInvite = combineReducers({
  inviteRequest
});

Similarly I'd expect App.js to look like

const loggedPerson = (state = {}, action) {
  switch (action.type) {
    case  `${actions.REQUEST_INVITE}_FULFILLED`:
      return {
        ...state,
        ...parseRemotePersonJSON(action.payload.person),
        authToken: state.authToken
      };
    // ...other cases...
    default:
      return state;
  }
};

export const app = combineReducers({
  loggedPerson
});

Does this make sense?

@gastonmorixe
Copy link
Author

@gaearon. First things first. You are the most awesome and kind person on the Internet. Period. Know it.

Second. It makes sense and the reason I don't want to do that is because of "separation of concerns" (?)

The "app" reducer is used at init time to los a bunch of data from obj-c (custom module)

The invites has nothing to do with it.

Say 1 month later I want to remove invite stuff completely. I want to remove just one piece of code, not search among all reducers for "where may I have reacted to some invite stuff?" That would be chaos in a big app.

What am I missing?

@gaearon
Copy link

gaearon commented Dec 23, 2015

How different parts of state react to the same action is what Redux considers different concerns. This allows different people on the team to work on different parts of the app that may respond to or fire the same actions—but don't have to share any mutation logic, and thus avoid constant merge conflicts.

In the situation you describe the solution is to grep the code base for the action name you wish to remove. It's not really hard to remove a corresponding switch case from every reducer, and it's much less fragile to split that logic than to keep it in a single place. For example if you later want to change the state shape of one of the reducers that are currently invite-aware, your code won't break because some outer "cross-cutting" reducer relied on its state shape.

@gastonmorixe
Copy link
Author

@gaearon great, I get your points.

Still, one more thing.

If instead of solving this invite request thing using redux-promise-middleware (REQUEST_INVITE_PENDING, REQUEST_INVITE_FULFILLED, REQUEST_INVITE_REJECTED), doing so using sagas and having one and only saga that takes care of the inviting stuff.

It would watch REQUEST_INVITE and when resolved it would do something like this
yield put( appActions.preloadData( {personData: {firstName: "Jorge"}}) )

This way, the invitation thing is only managed and understood in one place. I still get the part that it might become a mess later on in big apps because you can not check the "app" reducer and instantly get "who changed this state".

So the question final question is, is a matter of preference or I am still missing a better approach?

Thank you so much. 🎄

@gastonmorixe
Copy link
Author

for historical and educational reasons, the happy ending

from dan: @gastn___ I like the last approach you suggested. 

https://twitter.com/dan_abramov/status/679832619333345280

@phamcharles
Copy link

@imton,

I'm currently in the boat you were in during the time of this gist. I was hoping you could help shed some light. How did you handle higher order components/reducers that depended on other primitive reducers and do so with async fetch calls? Did redux-promise-middleware/redux-saga seem end up feeling like a good fit?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment