<-- /notes<-- /notes/learn-redux

Joining Reducers

Something important to note about Redux is that you can't simply import all your reducers at once into your store. Your store requires one root reducer, and it's up to you to do that. Of course you could dodge this problem by making all your reducers in the same file, but this can lead to a disorganized file structure.

Thankfully, Redux includes a nifty helper function for joining your reducers:

// ./reducers/index.js

import { combineReducers } from "redux"
import { routerReducer } from "react-router-redux"

// Some reducers
import posts from "./posts"
import comments from "./comments"

const rootReducer = combineReducers({
  posts,
  comments,
  routing: routerReducer,
})

export default rootReducer

This guy will combine all your reducers into one which can be used as the root for your Redux store.

Note: We've also added the routing reducer, which comes from react-router-redux. This is useful for directing our routing with information from the redux store, and it requires the exact reducer title to be routing, when being instantiated.


Reducer Composition

One of the fundamental patterns of building Redux apps is the ability to offload work into subreducers. These are kind of like helper functions which don't take in the entire state for modification, but can instead operate on just a slice that is passed to it.

Think about this; your store (or your global state) is an object. That object could have arrays, which contain objects, whose keys contain arrays, booleans and more objects and even just reading that was a little bit too hard. Trying to keep every piece of state the same in our pure function reducer is difficult.

This can be simplified if we break up our operations into subreducers, using these guys to make modifications to smaller pieces of data. Take this for example:

function postComments(state = [], action) {
  switch (action.type) {
    case "ADD_COMMENT":
      // Add a comment to the nested state
      return [...state, { user: action.author, text: action.comment }]
    case "REMOVE_COMMENT":
      const i = action.index
      return [
        // everything before this post
        ...state.slice(0, i),
        // everything after this post
        ...state.slice(i + 1),
      ]
    default:
      return state
  }
}

function comments(state = [], action) {
  if (typeof action.postId !== "undefined") {
    return {
      ...state,
      // Offload the nested state to a subreducer
      [action.postId]: postComments(state[action.postId], action),
    }
  }
  return state
}

With that code, we don't have to worry about ensuring all the data is returned in one step with some complicated nested logic, we get a logical, operational reducer!


Providing Access

In order for your store to be effective, you need to Provide access to its data throughout your application. You can do this by invoking the connect function on the component of your choice! In some cases, it's best to create a sort of wrapper component which will effectively prop-ify your main component. In the following snippet, we create a connect() wrapper component called App to replace Main.

// App.js
import { bindActionCreators } from "redux"
import { connect } from "react-redux"
import * as actionCreators from "../actions/actionCreators"
import Main from "./Main"

const mapStateToProps = s => {
  return { posts: s.posts, comments: s.comments }
}

const mapDispatchToProps = dispatch => {
  return bindActionCreators(actionCreators, dispatch)
}

const App = connect(
  mapStateToProps,
  mapDispatchToProps,
)(Main)

export default App

Now we simply replace Main with App in our router, and we can access the global state via the Main component's props!


Hot Reloading Redux Reducers (w/ Webpack)

Commonly with create-react-app applications, we'd like to set up our webpack to hot-reload on any change, but Redux reducers will usually through an error telling you that they require a hard reload. Instead, we can just modify our webpack build in order to let swap out our reducers whenever we should hot-reload them.

// In ./store.js
if (module.hot) {
  module.hot.accept("./reducers/", () => {
    const newRootReducer = require("./reducers/index").default
    store.replaceReducer(newRootReducer)
  })
}

Effectively, this code is swapping the existing reducer with the new one (which has been modified on save), whenever webpack is not up-to-date, and would be hot-reloading.


Using Enhancers

Redux is also capable of allowing for debugging via time-travelling through the operation history. This is used most commonly via the Redux DevTools (browser extension). Unlike React, Redux DevTools don't simply hook up to the app by default, they have to be connected. To do so, we specify the enhancer and the store as follows:

// Specify `compose`
import { createStore, compose } from "redux"

// Allow for the Redux DevTools to run on this app
const enhancers = compose(
  window.devToolsExtension ? window.devToolsExtension() : f => f,
)

const store = createStore(rootReducer, defaultState, enhancers)

Reducer Restrictions

Simply put, you cannot use asynchronous code in your reducers, they will NOT work! If there is ever a need to do so there are external libraries/packages which may help, namely redux-saga or redux-thunk. The author of Redux also made a helpful package for dealing with heavily nested JSON responses called normalizr, so that could also be of assistance.