This is documentation around a redux pattern I’ve adopted for our project with the United Way of Central Indiana. It is centered around the idea of keeping as much redux functionality as close to each other as possible.

File Structure

Everything related to redux is stored in app/state. Like you’d see across any redux implementation, each reducer is aligned around a product vertical, such as signin, onboarding, profile, etc.

Reducer

In app/state/signin.js:

export const SET_SIGNIN_EMAIL = 'SET_SIGNIN_EMAIL'
export const setSigninEmail = (email) => ({
  type: SET_SIGNIN_EMAIL,
  email,
})

const initialState = {
  email: ''
}

export const signinReducer = (state = initialState, action) => {
  switch (action.type) {
    case SET_SIGNIN_EMAIL:
      return {...state, email: action.email}
    default:
      return state
  }
}

Thunks

Thunks are ridiculously important; any code related to interfacing with your API should always live within a thunk.

  • Put thunks at the top of your reducer file.
  • Try to pass in no arguments at all. If they need access to state, get that state from within the thunk.
  • Make state changes from within the thunk, but pass UI changes through to a callback that is passed in.

In app/state/signin.js:

export const signinUser = (done) => (
  (dispatch, getState) => {
    dispatch(setSigninLoading(true))
    LoginUserWithApi({
      email: getState().signin.email,
      password: getState().signin.password,
    }, (err, user) => {
      dispatch(setSigninLoading(false))
      if (err) {
        dispatch(setSigninError(err))
      } else {
        dispatch(setActiveUser(user))
      }
      done(err, user)
    })
  }
)

State

In app/state/index.js

import {
  applyMiddleware,
  combineReducers,
  createStore,
} from 'redux'
import { createLogger } from 'redux-logger'
import thunk from 'redux-thunk'

// Import each reducer you create here
import { signinReducer } from './signin'

// Add them to the combineReducers call below
export const store = createStore(
  combineReducers({
    signin: signinReducer,
  }),
  applyMiddleware(
    thunk,
    createLogger()
  ),
)

// And also export everything from each reducer file,
// as those are the files that contain the actions we are dispatching.
export * from './signin'

Now, when any component needs an action, it can be imported directly from app/state.

Namespacing

Larger apps may want to namespace actions by their reducer. This is a good practice, and this pattern can be modified to support that well.

In app/state/signin.js

export const Signin = {
  signinUser: // Thunk Here
  SET_EMAIL: 'Signin.SET_EMAIL',
  setEmail: // Normal action here
}

const initialState = { /* ... */ }

export const signinReducer = // Normal reducer here

In app/state/index.js

// ...
// The only difference is the exports at the bottom
export Signin from './signin'

Now when you want to import them from a component…

import { Signin } from 'app/state'
// dispatch(Signin.setEmail('hello'))

Component

  • Always use mapStateToProps and mapDispatchToProps.
  • Treat your connected component like a truly separate component; this means separating it into a different file.

In app/views/signin/SigninContainer.jsx

import { Signin } from 'app/state'
import SigninComponent from './Signin'

export default connect(({ signin }) => ({
  email: signin.email,
}), (dispatch) => ({
  signin: () => dispatch(Signin.signinUser())
}), SigninComponent)

Action/Reducer Shortcut

If I’m working on a really quick prototype and just need to wire something up quick, I will create redux actions that look like this:

In app/state/signin.js

export const SET_SIGNIN_FIELD = 'SET_SIGNIN_FIELD'
export const setSigninField = (field, value) => ({
  type: SET_SIGNIN_EMAIL,
  field,
  value
})

const initialState = {
  email: ''
}

export const signinReducer = (state = initialState, action) => {
  switch (action.type) {
    case SET_SIGNIN_FIELD:
      return {...state, [action.field]: action.value}
    default:
      return state
  }
}

In app/views/signin/SigninContainer.jsx

import { setSigninField } from 'app/state'
import SigninComponent from './Signin'

export default connect(({ signin }) => ({
  email: signin.email,
}), (dispatch) => ({
  setEmail: (email) => dispatch(setSigninField('email', email)),
}), SigninComponent)

Is this perfect? Of course not. But the nice thing is that the API of your components stays exactly the same, so when it comes time to harden up that redux state, all you have to change are your actions and your containers.