Search code examples
javascriptreactjsreduxredux-thunk

Redux handling domain specific actions in a global component


My Redux app is broken down into individual components for the sake of modularity with the app domain being a kind of a shared infrastructure component which handles common ui changes like loading indicators.

app/ <- Global app  
---app_reducer.js  
---constants.js  
---actions.js
---...(react components)
users/
---users_reducer.js
---constansts.js
---actions.js
---...(react components)
products/
---products_reducer.js
---constants.js
---actions.js
---...(react components)

In my root reducer I use the combineReducers function to nicely split the state tree according to my domain:

import { combineReducers } from 'redux';
import App from './app/app_reducer'
import Users from './users/users_reducers'
import Products from './products/products_reducers'

export default combineReducers({
  App,
  Users,
  Products
})

So my state tree looks like this:

{
   app: 
     isLoading: false,
   users:
     ['user1', 'user2', 'user3']
   products:
     ['product1', 'product2'],
     lastFetch: 1246578942621
}

Ok, so now I want to add some async functionality to users and products where they fetch data from the server. This is trivial using something like redux-thunk or a promise middleware. So the action creator for users/actions.js might look like this:

import { LOAD_USERS_REQUEST, LOAD_USERS_SUCCESS } from './actions'

// Using redux-thunk
export function LoadUsers(){
  return function(dispatch){
     dispatch({type: LOAD_USERS_REQUEST})
     fetch('myserver.com/users')
     .then(function(data){
        dispatch({type: LOAD_USERS_SUCCESS, data })
     })
  }
}

Keep in mind that products might do something similar to fetch itself too.
Now, usersReducer handles these actions and sets different state props on the users subtree. Problem is, I would like to set a loading: true in the app subtree also so that my high level app component would render a nice loading indicator. But usersReducer doesn't get that slice of the state tree.

Without writing a custom combineReducers that will send all reducers the whole state tree what is the elegant way to implement this? I'm looking for a clean and scalable solution. Here's a couple approaches I considered:

1) Make appReducer respond to each new request. This means that each time I create a new action type for fetch, I need to add it in app/app_reducer.js:

import * as productTypes from '../products/constants'
import * as userTypes from '../users/constants'

export default function(state = {}, action){
  switch(action.type){
    case productTypes.LOAD_USERS_REQUEST:
    case productTypes.LOAD_PRODUCTS_REQUEST:
      return({...state, isLoading: true});
    case productTypes.LOAD_USERS_SUCCESS:
    case productTypes.LOAD_PRODUCTS_SUCCESS:
      return({...state, isLoading: false})
    default:
      return(state);
  }
}

The problem with this is that I'll have to modify appReducer with each new action I create that requires a loading indicator.

2) The second approach is to create two more actions called startLoading and stopLoading which will be invoked from the async action like so:

import { LOAD_USERS_REQUEST, LOAD_USERS_SUCCESS } from './actions'
import { START_LOADING, STOP_LOADING } from '../app/actions'    

export function LoadUsers(){
  return function(dispatch){
     dispatch({type: START_LOADING})
     dispatch({type: LOAD_USERS_REQUEST})
     fetch('myserver.com/users')
     .then(function(data){
        dispatch({type: STOP_LOADING})
        dispatch({type: LOAD_USERS_SUCCESS, data })
     })
  }
}

On first thought this seemed like a good idea but invoking 2 actions instead of one looks so wasteful.

I guess this is a common problem but I could not find any practical and "real life" suggestions out there. I'd be happy to hear any other approaches to this problem or thoughts about the given suggestions.


Solution

  • This topic is covered in the Redux FAQ. At the moment, yes, the answers are "write additional custom reducer logic", or "use getState to put more data into actions".

    That said, there is a PR currently being discussed that would let combineReducers pass down the entire state as an additional argument. See Feature Request: Allow reducers to consult global state.

    Also, to answer Pavel Tarno's comment: yes, each dispatch does call each sub-reducer function, if you're using combineReducers (see Redux FAQ ); and yes, each dispatch will then invoke all subscribers; but the React Redux connect() function will only force re-renders for subscribed components which return modified/updated results from mapStateToProps. Also, because of React's batching, even if multiple successive dispatches in an event cycle each caused a given component to need to re-render, there would probably only be one actual re-render. (Finally, it's "virtual DOM", not "shadow DOM".)