Immutability in JavaScript Using Redux
In an ever growing ecosystem of rich and complicated JavaScript applications, there’s more state to be managed than ever before: the current user, the list of posts loaded, etc.Managing state can be hard and error prone, but immutability and Redux- a predictable state container for JavaScript apps- can help significantly.
In this article, Toptal Programmer David Xu talks about managing state using immutability with Redux, a predictable state container.
In an ever growing ecosystem of rich and complicated JavaScript applications, there’s more state to be managed than ever before: the current user, the list of posts loaded, etc.Managing state can be hard and error prone, but immutability and Redux- a predictable state container for JavaScript apps- can help significantly.
In this article, Toptal Programmer David Xu talks about managing state using immutability with Redux, a predictable state container.
David has taken several mobile apps from an idea to millions of users worldwide as the Chief Architect of Castle Global, Inc.
Expertise
PREVIOUSLY AT
In an ever growing ecosystem of rich and complicated JavaScript applications, there’s more state to be managed than ever before: the current user, the list of posts loaded, etc.
Any set of data that needs a history of events can be considered stateful. Managing state can be hard and error prone, but working with immutable data (rather than mutable) and certain supporting technologies- namely Redux, for the purposes of this article- can help significantly.
Immutable data has restrictions, namely that it can’t be changed once it’s created, but it also has many benefits, particularly in reference versus value equality, which can greatly speed up applications that rely on frequently comparing data (checking if something needs to update, for example).
Using immutable states allows us to write code that can quickly tell if the state has changed, without needing to do a recursive comparison on the data, which is usually much, much faster.
This article will cover the practical applications of Redux when managing state through action creators, pure functions, composed reducers, impure actions with Redux-saga and Redux Thunk and, finally, use of Redux with React. That said, there are a lot of alternatives to Redux, such as MobX, Relay, and Flux based libraries.
Why Redux?
The key aspect that separates Redux from most other state containers such as MobX, Relay, and most other Flux based implementations is that Redux has a single state that can only be modified via “actions” (plain JavaScript objects), which are dispatched to the Redux store. Most other data stores have the state contained in React components themselves, allow you to have multiple stores and/or use mutable state.
This in turn causes the store’s reducer, a pure function that operates on immutable data, to execute and potentially update the state. This process enforces unidirectional data flow, which is easier to understand and more deterministic.
Since Redux reducers are pure functions operating on immutable data, they always produce the same output given the same input, making them easy to test. Here’s an example of a reducer:
import Immutable from 'seamless-immutable'
const initialState = Immutable([]) // create immutable array via seamless-immutable
/**
* a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state.
*/
function addUserReducer(state = initialState, action) {
if (action.type === 'USERS_ADD') {
return state.concat(action.payload)
}
return state // note that a reducer MUST return a value
}
// somewhere else...
store.dispatch({ type: 'USERS_ADD', payload: user }) // dispatch an action that causes the reducer to execute and add the user
Dealing in pure functions allows Redux to easily support many use cases that are generally not easily done with mutative state, such as:
- Time travel (Going back in time to a previous state)
- Logging (Track every single action to figure out what caused a mutation in the store)
- Collaborative environments (Such as GoogleDocs, where actions are plain JavaScript objects and can be serialized, sent over the wire, and replayed on another machine)
- Easy bug reporting (Just send the list of actions dispatched, and replay them to get the exact same state)
- Optimized rendering (At least in frameworks that render virtual DOM as a function of state, such as React: due to immutability, you can easily tell if something has changed by comparing references, as opposed to recursively comparing the objects)
- Easily test your reducers, as pure functions can easily be unit tested
Action Creators
Redux’s action creators help in keeping code clean and testable. Remember that “actions” in Redux are nothing more than plain JavaScript objects describing a mutation that should occur. That being said, writing out the same objects over and over again is repetitive and error prone.
An action creator in Redux is simply a helper function that returns a plain JavaScript object describing a mutation. This helps reduce repetitive code, and keeps all your actions in one place:
export function usersFetched(users) {
return {
type: 'USERS_FETCHED',
payload: users,
}
}
export function usersFetchFailed(err) {
return {
type: 'USERS_FETCH_FAILED',
payload: err,
}
}
// reducer somewhere else...
const initialState = Immutable([]) // create immutable array via seamless-immutable
/**
* a reducer takes a state (the current state) and an action object (a plain JavaScript object that was dispatched via dispatch(..) and potentially returns a new state.
*/
function usersFetchedReducer(state = initialState, action) {
if (action.type === 'USERS_FETCHED') {
return Immutable(action.payload)
}
return state // note that a reducer MUST return a value
}
Using Redux with Immutable Libraries
While the very nature of reducers and actions make them easy to test, without an immutability helper library, there’s nothing protecting you from mutating objects, meaning the tests for all your reducers have to be particularly robust.
Consider the following code example of a problem you’ll run into without a library to protect you:
const initialState = []
function addUserReducer(state = initialState, action) {
if (action.type === 'USERS_ADD') {
state.push(action.payload) // NOTE: mutating action!!
return state
}
return state // note that a reducer MUST return a value
}
In this code example, time travel will be broken as the previous state will now be the same as the current state, pure components may potentially not update (or re-render) as the reference to the state has not changed even though the data it contains has changed, and mutations are a lot harder to reason through.
Without an immutability library, we lose all the benefits that Redux provides. It’s therefore highly recommended to use an immutability helper library, such as immutable.js or seamless-immutable, especially when working in a large team with multiple hands touching code.
Regardless of which library you use, Redux will behave the same. Let’s compare the pros and cons of both so that you’re able to pick whichever one is best suited for your use case:
Immutable.js
Immutable.js is a library, built by Facebook, with a more functional style take on data structures, such as Maps, Lists, Sets, and Sequences. Its library of immutable persistent data structures perform the least amount of copying possible in between different states.
Pros:
- Structural sharing
- More efficient at updates
- More memory efficient
- Has a suite of helper methods to manage updates
Cons:
- Does not work seamlessly with existing JS libraries (i.e lodash, ramda)
- Requires conversion to and from (toJS / fromJS), especially during hydration / dehydration and rendering
Seamless-immutable
Seamless-immutable is a library for immutable data that is backwards compatible all the way to ES5.
It’s based on ES5 property definition functions, such as defineProperty(..)
to disable mutations on objects. As such, it is fully compatible with existing libraries like lodash and Ramda. It can also be disabled in production builds, providing a potentially significant performance gain.
Pros:
- Works seamlessly with existing JS libraries (i.e lodash, ramda)
- No extra code needed to support conversion
- Checks can be disabled in production builds, increasing performance
Cons:
- No structural sharing - objects / arrays are shallow-copied, makes it slower for large data sets
- Not as memory efficient
Redux and Multiple Reducers
Another useful feature of Redux is the ability to compose reducers together.This allows you to create much more complicated applications, and in an application of any appreciable size, you will inevitably have multiple types of state (current user, the list of posts loaded, etc). Redux supports (and encourages) this use case by naturally providing the function combineReducers
:
import { combineReducers } from 'redux'
import currentUserReducer from './currentUserReducer'
import postsListReducer from './postsListReducer'
export default combineReducers({
currentUser: currentUserReducer,
postsList: postsListReducer,
})
With the above code, you can have a component that relies on the currentUser
and another component that relies on the postsList
. This also improves performance as any single component will only be subscribing to whatever branch(es) of the tree concerns them.
Impure Actions in Redux
By default, you can only dispatch plain JavaScript objects to Redux. With middleware, however, Redux can support impure actions such as getting the current time, performing a network request, writing a file to disk, and so on.
‘Middleware’ is the term used for functions that can intercept actions being dispatched. Once intercepted, it can do things like transform the action or dispatch an asynchronous action, much like middleware in other frameworks (such as Express.js).
Two very common middleware libraries are Redux Thunk and Redux-saga. Redux Thunk is written in an imperative style, while Redux-saga is written in a functional style. Let’s compare both.
Redux Thunk
Redux Thunk supports impure actions within Redux by using thunks, functions that return other chain-able functions. To use Redux-Thunk, you must first mount the Redux Thunk middleware to the store:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
const store = createStore(
myRootReducer,
applyMiddleware(thunk), // here, we apply the thunk middleware to R
)
Now we can perform impure actions (such as performing an API call) by dispatching a thunk to the Redux store:
store.dispatch(
dispatch => {
return api.fetchUsers()
.then(users => dispatch(usersFetched(users)) // usersFetched is a function that returns a plain JavaScript object (Action)
.catch(err => dispatch(usersFetchError(err)) // same with usersFetchError
}
)
It’s important to note that using thunks can make your code hard to test and makes it harder to reason through code flow.
Redux-saga
Redux-saga supports impure actions through an ES6 (ES2015) feature called generators and a library of functional / pure helpers. The great thing about generators is that they can be resumed and paused, and their API contract makes them extremely easy to test.
Let’s see how we can improve readability and testability of the previous thunk method using sagas!
First, let’s mount the Redux-saga middleware to our store:
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import rootReducer from './rootReducer'
import rootSaga from './rootSaga'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount the middleware to the store
const store = createStore(
rootReducer,
applyMiddleware(sagaMiddleware),
)
// run our saga!
sagaMiddleware.run(rootSaga)
Note that the run(..)
function must be called with the saga for it to begin executing.
Now let’s create our saga:
import { call, put, takeEvery } from 'redux-saga/effects' // these are saga effects we'll use
export function *fetchUsers(action) {
try {
const users = yield call(api.fetchUsers)
yield put(usersFetched(users))
} catch (err) {
yield put(usersFetchFailed(err))
}
}
export default function *rootSaga() {
yield takeEvery('USERS_FETCH', fetchUsers)
}
We defined two generator functions, one that fetches the users list and the rootSaga
. Notice that we didn’t call api.fetchUsers
directly but instead yielded it in a call object. This is because Redux-saga intercepts the call object and executes the function contained within to create a pure environment (as far as your generators are concerned).
rootSaga
yields a single call to a function called takeEvery,
which takes every action dispatched with a type of USERS_FETCH
and calls the fetchUsers
saga with the action it took. As we can see, this creates a very predictable side effect model for Redux, which makes it easy to test!
Testing Sagas
Let’s see how generators make our sagas easy to test. We’ll be using mocha in this part to run our unit tests and chai for assertions.
Because sagas yield plain JavaScript objects and are run within a generator, we can easily test that they perform the right behavior without any mocks at all! Keep in mind that call
, take
, put
, etc are just plain JavaScript objects that are intercepted by the Redux-saga middleware.
import { take, call } from 'redux-saga/effects'
import { expect } from 'chai'
import { rootSaga, fetchUsers } from '../rootSaga'
describe('saga unit test', () => {
it('should take every USERS_FETCH action', () => {
const gen = rootSaga() // create our generator iterable
expect(gen.next().value).to.be.eql(take('USERS_FETCH')) // assert the yield block does have the expected value
expect(gen.next().done).to.be.equal(false) // assert that the generator loops infinitely
})
it('should fetch the users if successful', () => {
const gen = fetchUsers()
expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded
const users = [ user1, user2 ] // some mock response
expect(gen.next(users).value).to.be.eql(put(usersFetched(users))
})
it('should fail if API fails', () => {
const gen = fetchUsers()
expect(gen.next().value).to.be.eql(call(api.fetchUsers)) // expect that the call effect was yielded
const err = { message: 'authentication failed' } // some mock error
expect(gen.throw(err).value).to.be.eql(put(usersFetchFailed(err))
})
})
Working with React
While Redux isn’t tied to any specific companion library, it works especially well with React.js since React components are pure functions that take a state as input and produce a virtual DOM as output.
React-Redux is a helper library for React and Redux that eliminates most of the hard work connecting the two. To most effectively use React-Redux, let’s go over the notion of presentational components and container components.
Presentational components describe how things should look visually, depending solely on their props to render; they invoke callbacks from props to dispatch actions. They’re written by hand, completely pure, and are not tied to state management systems like Redux.
Container components, on the other hand, describe how things should function, are aware of Redux, dispatch Redux actions directly to perform mutations and are generally generated by React-Redux. They are often paired with a presentational component, providing its props.
Let’s write a presentational component and connect it to Redux via React-Redux:
const HelloWorld = ({ count, onButtonClicked }) => (
<div>
<span>Hello! You've clicked the button {count} times!</span>
<button onClick={onButtonClicked}>Click me</button>
</div>
)
HelloWorld.propTypes = {
count: PropTypes.number.isRequired,
onButtonClicked: PropTypes.func.isRequired,
}
Note that this is a “dumb” component that completely relies on its props to function. This is great, because it makes the React component easy to test and easy to compose. Let’s look at how to connect this component to Redux now, but first let’s cover what a Higher Order Component is.
Higher Order Components
React-Redux provides a helper function called connect( .. )
that creates a higher order component from a “dumb” React component that is aware of Redux.
React emphasizes extensibility and re-usability through composition, which is when you wrap components in other components. Wrapping these components can change their behavior or add new functionality. Let’s see how we can create a higher order component out of our presentational component that is aware of Redux - a container component.
Here’s how you do it:
import { connect } from 'react-redux'
const mapStateToProps = state => { // state is the state of our store
// return the props that we want to use for our component
return {
count: state.count,
}
}
const mapDispatchToProps = dispatch => { // dispatch is our store dispatch function
// return the props that we want to use for our component
return {
onButtonClicked: () => {
dispatch({ type: 'BUTTON_CLICKED' })
},
}
}
// create our enhancer function
const enhancer = connect(mapStateToProps, mapDispatchToProps)
// wrap our "dumb" component with the enhancer
const HelloWorldContainer = enhancer(HelloWorld)
// and finally we export it
export default HelloWorldContainer
Note that we defined two functions, mapStateToProps
and mapDispatchToProps
.
mapStateToProps
is a pure function of (state: Object) that returns an object computed from the Redux state. This object will be merged with the props passed to the wrapped component. This is also known as a selector, since it selects parts of the Redux state to be merged into the component’s props.
mapDispatchToProps
is also a pure function, but one of (dispatch: (Action) => void) that returns an object computed from the Redux dispatch function. This object will likewise be merged with the props passed to the wrapped component.
Now to use our container component we must use the Provider
component in React-Redux to tell the container component what store to use:
import { Provider } from 'react-redux'
import { render } from 'react-dom'
import store from './store' // where ever your Redux store resides
import HelloWorld from './HelloWorld'
render(
(
<Provider store={store}>
<HelloWorld />
</Provider>
), document.getElementById('container')
)
The Provider
component propagates the store down to any child components who subscribe to the Redux store, keeping everything in one place and reducing points of error or mutation!
Build Code Confidence With Redux
With this newfound knowledge of Redux, its numerous supporting libraries and its framework connection with React.js, you can easily limit the number of mutations in your application through state control. Strong state control, in turn, lets you you move faster and create a solid code base with more confidence.
David Xu
Denver, CO, United States
Member since October 20, 2016
About the author
David has taken several mobile apps from an idea to millions of users worldwide as the Chief Architect of Castle Global, Inc.
Expertise
PREVIOUSLY AT