Redux API Review

Out of all of the libraries that I use on a regular basis, there aren't many that have a small enough API to cover in one post. Redux is one of the ones where we can do that, and since it's so useful I'm going to.

Redux exports 5 top level functions and a handful of helpers for glue. They are -

createStore(reducer, [initialState])
combineReducers(reducers)
applyMiddleware(...middlewares)
bindActionCreators(actionCreators, dispatch)
compose(...functions)

createStore(reducer, [initialState])

The first function we get from Redux is createStore, this function takes a function that reduces app state and an optional initial state parameter. It's partially applied so that the first time you call it you get back an object that holds your app's state in the form of 4 methods.

import { createStore } from 'redux'

function greet(state = 'Hi!', action) {  
  switch(action.type) {
    case 'HELLO':
      return `Hi!,${action.name}`
    case 'YO':
      return `Yo!, ${action.name}`
  }
}

let store = createStore(greet)  

At this point, we've created the minimal state we need for our to greet someone.

The store object now contains the information we need to dispatch actions and get new state back from our store. It gives us 4 methods as we said before. They are -


dispatch(action)
subscribe(listener)
getState()
replaceReducer(nextReducer)

store.getState just returns whatever the current state is, you can call this at anytime to get your state tree. This value was either passed in as initial state or updated by an action call to a reducer.

In order to update the state tree, you can call store.dispatch(action) which will trigger your reducers to test for a matching action and update the state. By convention, actions look like this -

function greet(name) {  
  return { type: 'YO', name }
}

Using constants as action types helps with consistency and maintainability on bigger projects.

Our store passes the object we get from our greet action creator to dispatch and all that dispatch does is give the state and action object to reducers and notify subscribers of the new state.

store.subscribe gives you the option to be notified when a part of the store has changed. It does this by keeping an array of listeners and exposing and unsubscribe function that has access to the listeners via a closure. The dispatch function uses this list of listeners to make notifications about changes.

Libraries like react-redux use store.subscribe to extract away the part where we connect our top level components to stores. Check out the connect function in the source for react-redux. You'll see that it is a High Order Component that wraps a component with functionality to get updates from dispatches using store.subscribe. What we get back is a component that gets state from our store.

The last method from createStore is store.replaceReducer. This function replaces the current reducer with one passed in. This is useful for loading async reducers. Here's an example of the only use case I can think of, which is breaking out your app into multiple bundles.


combineReducers(reducers)

combineReducers is a helper function that takes your reducers and builds the state free from the combined output of all your reducers. The cool thing about the way Redux is built is that once you understand it well, you can replace parts of it when you feel the need. This function returns the initial state when a piece of the state is undefined but you can write your own combinator that handles state however you want.

The important part of combineReducers looks like this -

export default function combineReducers(reducers) {  
  var finalReducers = pick(reducers, (val) => typeof val === 'function')

  return function combination(state = {}, action) {
    var hasChanged = false
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var previousStateForKey = state[key]
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
      return nextStateForKey
    })

    return hasChanged ? finalState : state
  }
}

I stripped out some safety check stuff for brevity, but basically this is the function that we get back from combineReducers. combination has closure access to the reducers and can take actions and apply them to each reducer, then return the state.


applyMiddleware(...middlewares)

Middleware is a really cool concept that works really well with the one-state-tree approach that Redux takes. It returns a function that can be applied to createStore. This lets you apply whatever functionality you want to all actions. Some good use cases are async actions and loggers. This approach applies these changes in one place and they affect your entire data layer making it very maintainable.

applyMiddleware take any number of middleware and then returns a function which can apply middleware to your store's dispatch.

export default function applyMiddleware(...middlewares) {  
  return (next) => (reducer, initialState) => {
    var store = next(reducer, initialState)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

Here are a couple of useful middleware packages to take a look at.

redux-thunk
redux-logger

bindActionCreators(actionCreators, dispatch)

There are 2 ways to let components that are not connected to stores have the ability to create actions. You can pass down the dispatch function and then import actions to pass into the dispatch or you can use bindActionCreators. The difference comes down to whether you want to manage your actions in one top level component or multiple ones. A good example again is found in the react-redux package. We can see that they use a utility function that wraps bindActionCreators in the connect call in order to pass down actions through props to React. All bindActionCreators really does is call an action creator function and pass the results to dispatch.


compose(...functions)

Compose is a utility function that can be used to apply middleware to your stores in a convenient way. The whole thing is less than 20 lines and it can be described as a partially applied function that gathers the middleware the first time it's called and applies it to the store on the second call.

export default function compose(...funcs) {  
  return (...args) => {
    if (funcs.length === 0) {
      return args[0]
    }

    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)

    return rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

It can be used like this -

// import the middleware
let middleware = [ thunk, logger ]

finalCreateStore = applyMiddleware(...middleware)(createStore)  

That's the entire library. We are able to understand every piece and even replace pieces of it if they become irrelevant. Simplicity is king!

Thanks for reading and please thank @dan_abramov for his work. 👍