A brief introduction to Redux



I've been working on a couple of large React projects lately, both were using straight up React + Flux. I was pretty new to React prior to these projects, but I fell in love with it pretty quickly.

Coming from Angular 1 previously, React was a complete overhaul in everything I knew about client-side Javascript. For instance, writing html templates within your Javascript (JSX)...

class MyComponent extends React.Component {  
    render() {
        return(
            <p>This is my component</p>
        );
    }
}

This felt really strange, but I liked it.

Flux

Flux was also something else I was totally new to, but again, it made sense. Having previously battled with double-bound states, overriding each other and generally becoming quite tedious to keep track of and debug. Flux promised to solve that problem, and it did for the most part.

Flux takes the 'single source of truth' theory. Where a state should have one single source, so you can track the full flow of data in your application.

Component

class MyComponent extends React.Component {

  constructor() {
    this.state = {
      thing: '',
      thingTwo: ''
    }

    this.onChange = this.onChange.bind(this);
  }

  componentDidMount() {

    // When component mounts, listen to stores
    // to call `onChange` when a change is emitted.
    // And fire off any actions
    ThingStore.addChangeListener(this.onChange);
    ThingActions.getThing();  

    StuffStore.addChangeListener(this.onChange);
    StuffActions.getStuff();
  }

  componentWillUnmount() {
    // When component unmounts, remove change listeners from stores
    ThingStore.removeChangeListener(this.onChange);
    StuffStore.removeChangeListener(this.onChange);
  }

  onChange() {
    this.setState({
      thing: ThingStore.thing,
      stuff: StuffStore.stuff
    });
  }

  render() {
    return (
      <p>{this.state.thing} - {this.state.stuff}</p>
    );
  }
}

ThingStore

class ThingStore extends BaseStore {

  constructor() {
    this.subscribe(() => this._registerToActions.bind(this));
    this._thing = '';
  }

  get thing() {
    return this._thing;
  }

  _registerToActions(action) {
    switch(action.type) {
      case 'GOT_THING':
        this._thing = action.thing;
        // Tell any listening components something's changed
        this.emitChange();
    }
  }
}

StuffStore

class StuffStore extends BaseStore {

  constructor() {
    this.subscribe(() => this._registerToActions.bind(this));
    this._stuff = '';
  }

  get stuff() {
    return this._stuff;
  }

  _registerToActions(action) {
    switch(action.type) {
      case 'GOT_STUFF':
        this._thing = action.stuff;
        // Tell any listening components something's changed
        this.emitChange();
    }
  }
}

I've omitted actions as it's mostly stores I'm interested in here.

This worked pretty well, however it still felt tricky at times managing the are states across all of our different stores. I started to wonder if having all of these different stores and states were truly a 'single source of truth'. Then I started reading into Redux, which seemed to solve exactly that problem.

Redux

Redux does something ingenious, which takes some getting your head around at first. In Redux, there is one 'main' store, which container your entire applications state at any given point.

But isn't it an absolute mess containing all of our stores in just one large store? That's where reducers get added into the mix. This was perhaps the most tricky part to understand.

Reducers are like what our stores were before, they listen for actions and dispatch something to listening components accordingly. You then combine all of your reducers, which then keeps tabs of anything happening to the entire state of your application.

One other crucial aspect about reducers you need to understand is that reducers, unlike stores in our previous example do not mutate the state. Actually, it clones a copy of the state and apply the new changes instead. You can use libraries such as deep freeze to actually make the entire state immutable, just to really hammer home that point.

So why are they called 'reducers'? Well, we've all heard of map, reduce, right? Reduce takes data, and reduces it down to a single outcome. But how does that apply to Flux? Well, you pass your state and an action to your reducer, it applies the action to the state, and gives you the final state.

So what does our previous example look like?

Root component - here we create a root component to pass our state into, so it's accessible via props in child components.

import { Provider } from 'react-redux';  
import store from './stores/Store';  
const store = new store();

import MyComponent from './components/MyComponent';

class Root extends React.Component {  
  render() {
    return (
      <Provider store={store}>
        <MyComponent />
      </Provider>
    );
  }
}

Component

import { connect } from 'react-redux';  
import React, { Component, PropTypes } from 'react';

class MyComponent extends React.Component {

  constructor(props) {
    super(props);
    this.onChange = this.onChange.bind(this);
  }

  componentDidMount() {
    const { dispatch } = this.props;
    dispatch(getThing());
    dispatch(getStuff());
  }

  render() {
    const { thing, stuff } = this.props;
    return (
      <p>{thing} - {stuff}</p>
    );
  }
}

MyComponent.propTypes = {  
  thing: PropTypes.string.isRequired,
  stuff: PropTypes.string.isRequired
}

function mapStateToProps(state) {  
  const { thing, stuff } = state
  return {
    thing,
    stuff
  }
}

export default connect(mapStateToProps)(MyComponent);  

Now our single store...

import { createStore, applyMiddleware } from 'redux';

import thunk from 'redux-thunk';  
import createLogger from 'redux-logger';

import Reducers from '../reducers/Reducers';

const loggerMiddleware = createLogger();

export default function store(initState) {  
  return createStore(
    Reducers,
    initState,
    applyMiddleware(
      loggerMiddleware,
      thunk
    )
  )
}

That's it... the one and only store you'll be needing.

By this point, this probably doesn't make a lot of sense. Where's the state kept if the store's just that? Well, remember I mentioned 'reducers'?

Reducer - This is our root reducer, which combines all of our reducers into a single state.

import { combineReducers } from 'redux';  
import { thingReducer } from './ThingReducer';  
import { stuffReducer } from './StuffReducer';

const Reducers = combineReducers({  
  thingReducer,
  stuffReducer
});

export default Reducers;  

Now let's look at an individual reducer

StuffReducer

import StuffActions from '../actions/StuffActions';

export const stuff = (state = {  
  data: []
}, action) => {
  switch(action.type) {
    case StuffActions.GOT_STUFF:
      return Object.assign({}, state, {
      data: action.data
    })
    break;

    default:
      break;
}

This is essentially the same as the _registerToActions function we were using in our previous stores. However these are now isolated from our store, and you can see that we're assigning changes to an empty object to the new set of data when that action is picked up.

StuffActions

import fetch from 'isomorphic-fetch';

export const GET_STUFF = 'GET_STUFF';  
export const GOT_STUFF = 'GOT_STUFF';


function getStuffRequest() {  
  return {
    type: GET_STUFF
  }
}

export function getStuff() {  
  dispatch => {
    return fetch(`/api/v1/stuff`)
           .then(req => req.json())
           .then(json => dispatch(gotStuff(json)));
  }
}

function gotStuff(json) {  
  return {
    type: GOT_STUFF,
    data: json.data
  }
}

Just as we called our Redux store's central dispatcher in our component, we're making heavy use of dispatch again in our actions to orchestrate the flow of out actions and triggering our reducers, which are listening to our action calls.

So with a few small changes, your React application uses one, immutable state in the form of one single store. This store holds the entire application's state. Instead of us calling on several different stores, which hold a mutable state.

Checkout the redux example for a more in-depth overview.