JavaScript : Getting started with redux

15 Feb 2016

This is planned as part 2 of What I learned after building first web app with react post because I believe one of the biggest mistake I did was not investing much time in redux and went for states in components. Component states are difficult to manage and refactor.

Even though I heard of flux architecture and uni-directional data flow long back I didn’t really understand the concept and why we need it. But recently I got my hands on the redux and now I know why we shouldn’t be using component level states in react application.

Redux help you to have a centralized store for data. So all the data related to you app will be holding in a object instead of holding in different components.

If you getting started with redux I highly recommend to watch the Getting started with Redux by Dan Abramov creator of redux itself. This post is just my notes on my learning from this series.

Action

Actions are simple JavaScript object which used to send data to application store. All action object should have a type key to define the action along with data to be changed in store if any.

{
  type: 'INCREMENT'
}

Reducer

Reducers are the pure function which will change state and return new state. The only way to change state is to emit an action.

function counter(state=0, action) {
  switch(action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

Store

Store is the place where application state is holding. We can use the createStore from redux to create a new store using the reducer. The object returned from createStore will have 3 methods

  • getState() : Get the current application state
  • dispatch() : To dispatch an action to change the state
  • subscribe() : To listen to the state change
import {createStore} from 'redux';

const store = createStore(counter);

console.log(store.getState()) // logs 0

store.dispatch({
  type: 'INCREMENT'
});

console.log(store.getState()) // logs 1

Using it with React

import React, {Component} from 'react';
import {createStore} from 'redux';

// reducer 
function counter(state=0, action) {
  switch(action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

//create store
const store = createStore(counter);

// React Component
class Counter extends React.Component {
  increment() {
    this.props.store.dispatch({
      type: 'INCREMENT'
    });
  }
  decrement() {
    this.props.store.dispatch({
      type: 'DECREMENT'
    });
  }
  render() {
    return (<div>
      {this.props.store.getState()}
      <div>
        <button onClick={this.increment.bind(this)}>+</button>
        <button onClick={this.decrement.bind(this)}>-</button>
      </div>
    </div>
            )
  }
}
  
const render = () => {
  ReactDOM.render(<Counter store={store} />, document.getElementById('mount'));
};

store.subscribe(render);
render();

The above example can’t be used in real world applications since it’s not maintainable or testable. So we need better way to use redux with react. That’s where react-redux comes into picture.

Using react-redux bindings.

The react-redux bindings provide you Provider a Higher order component and a curried function called connect.

We need to wrap our top component using Provider and pass store to it. connect method helps to subscribe to store changes pass the state to our component as props. This will return a new connect component after subscribing to Redux store.

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

// reducer 
function counter(state=0, action) {
  console.log('counter', action)
  switch(action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

//create store
const store = createStore(counter);

// React Component

class Counter extends React.Component {
  increment() {
    this.props.dispatch({
      type: 'INCREMENT'
    });
  }
  decrement() {
    this.props.dispatch({
      type: 'DECREMENT'
    });
  }
  render() {
    return (<div>
      {this.props.state}
      <div>
        <button onClick={this.increment.bind(this)}>+</button>
        <button onClick={this.decrement.bind(this)}>-</button>
      </div>
    </div>
            )
  }
}

const mapStateToProps = function (state) {
  return {state};
}

const CounterApp = connect(mapStateToProps)(Counter);

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <CounterApp />
      </Provider>
    )
  }
} 

ReactDOM.render(<App />, document.getElementById('mount'));

Extract actions for maintainability

In the above example dispatch actions from the component itself. This will reduce the re-usability and testability. So for better maintainability we can extract the actions separately.

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

// reducer 
function counter(state=0, action) {
  console.log('counter', action)
  switch(action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}


//actions
const increment = () => {
  return {
    type: 'INCREMENT'
  };
};

const decrement = () => {
  return {
    type: 'DECREMENT'
  };
};

//create store
const store = createStore(counter);

// React Component
let Counter;
class Counter extends React.Component {
  render() {
    return (
      <div>
        {this.props.state}
        <div>
          <button onClick={this.props.increment}>+</button>
          <button onClick={this.props.decrement}>-</button>
        </div>
      </div>
    )
  }
}

const mapStateToProps = (state) {
  return {state};
};

const mapDispatchToProps = (dispatch) {
  return {
    decrement() {
      dispatch(decrement());
    },
    increment() {
      dispatch(increment());
    }
  }
};

let Counter = connect(mapStateToProps)(Counter);

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <Counter />
      </Provider>
    )
  }
} 

ReactDOM.render(<App />, document.getElementById('mount'));

Now after extracting the actions separately I can use the same action in the tests as well.

If you find my work helpful, You can buy me a coffee.