DEV Community

jaepass
jaepass

Posted on • Updated on

Learning Redux

Any fully functioning modern application will deal with relatively sizeable data these days. Whether it's a blog, an eCommerce store, or perhaps your own portfolio with a CMS, all of which would need to output some form of data. One of the main challenges of building a modern application is having to make sure all parts of your UI are synchronizing. Without a tool to abstract your data layer away, it can only become more cumbersome as your application scales. Data and state management in itself is a sizeable topic to cover. For the purpose of this article, I'll stick with high level concepts from what I've learned and put these concepts to practice by building a simple application.

What is Redux?

Redux in isolation is a library with neatly packaged helper functions for you to pull into your application to manage your application's state. Effectively, it is a state management tool that makes it easy to manage state across shared components. Redux provides a way to centralize all your data in one place called the store, and each component can have access to this store's data without having to send down props from one component to another. Some of the main functions in Redux that are commonly used are createStore, dispatch, bindActionCreators, which we will be using later to build our application.

Building block of Redux: Store, Actions, Reducers

The store is the state container where your data will live. Actions are event emitters to get the data out from our application to our Redux store. User inputs and API calls are examples of actions. Actions are then sent by using the store.dispatch(). Again, you will see this touched on later in the tutorial. Lastly, think of reducers like a funnel that takes in the initial state of your application, run some actions on it, and return out an updated state.

Now lets put Redux to work!

Redux is a fully agnostic library so for the purpose of seeing it in action, we are going build a basic Counter application with React. Theoretically, we could pull in all the functions provided by redux that we need and wire them up in our application, but a react-redux library already exists for this purpose.

Please note that this app and its file structure should not necessarily be implemented this way in practice. This guide is purely to walk through the high-level building blocks of Redux and how it works in a React application.

First, lets run up a React application by copying the below commands to your terminal.

npx create-react-app redux-counter
npm i react-redux redux
cd redux-counter
npm start

Open up your directory in your code editor and copy the below code into an index.js

// index.js

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

class Counter extends Component {
  render() {
    return (
      <main className="Counter">
    <p className="count">0</p>
    <section className="controls">
      <button>Increment</button>
      <button>Decrement</button>
      <button>Reset</button>
    </section>
      </main>
    );
  }
}

Currently, this component is fully static and doesn't do anything quite yet but we will get into making it functional. Let's start with explaining the imports at the top. We are of course importing React as we will be using the framework to build our application. Next, we are importing Redux and will be extracting the method createStore from it. Lastly, the methods connect and Provider are what we will be using to essentially "connect" our store and data with the the rest of our application.

Normally, for a decent-sized application, initial Redux setup has a boilerplate. Meaning, there is some structure to setting up your file system with functions and logic abstracted into separate files and wired up altogether. To comb through the concepts of redux in an application, we are going to be building it using only one file for clarity.

Now let's inject some Redux functions (place the code block after your imports):

// Our default initial state
const initialState = {
    count: 0,
}

// Our action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// These are action creators
const incrementValue = () => ({
    type: INCREMENT,
});

const decrementValue = () => ({
    type: DECREMENT,
});

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

const store = createStore(reducer);

Let's first have a look at our initialState variable and our reducer function. The initialState is the current snapshot of what the state looks like. We store the initial state of our application in a variable so that we can cleanly pull it in to the reducer function. With our reducer function, we start by passing in the state and the action. The application's state will default to initialState before any action is passed. We are always returning our state because every action will go through the reducer regardless of whether the reducer is affected by an action. We store our actions INCREMENT and DECREMENT into variables to prevent any future typos. We can also store the logic of our actions into functions which are called action creators in Redux. The type properties in our actions describes exactly what actions are being carried out. type needs to have string values, in this case, we've stored our values as variables for better error handling.

The reducer function takes in two arguments, the current state and the action. Think of the reducer as the pure JavaScript reduce method that takes in one value with a callback and returns a brand new object. We then use the switch statement with cases INCREMENT and DECREMENT and eventually return a new state. Lastly, we need to create our store with createStore() and pass in our reducer

Now that we have our store, we will need to hook it up to our application. This is where the react-redux library along with its connect and Provider methods come in handy.

render(
    <Provider store={store}>
        <Counter />
    </Provider>,
    document.getElementById('root')
)

The above code block will render out our app. First let's look at the Provider wrapper. Provider takes in a prop and that prop is our store that we've created. Without the prop, we would be not be able to access the store's state in our component. Remember that we are using redux strictly for handling our application state, we are not using React's built-in state management. So our store's data is being passed in as props in our components.

Now, how exactly do we connect the Redux store with our React application? We will use the connect method. Connect returns a function waiting for a React component. To break it down, it takes arguments of a function that maps the state to the component and a function that maps the action in.

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

const mapDispatchToProps = (dispatch) => {
  return {
    increment() { 
      dispatch(incrementValue()) 
    },
    decrement() { 
      dispatch(decrementValue()) 
    }
  }
}

const CounterWrapper = connect(mapStateToProps, mapDispatchToProps)(Counter)

render(
    <Provider store={store}>
        <CounterWrapper />
    </Provider>,
    document.getElementById('root')
)

Let's first look at mapStateToProps() and mapDispatchToProps() which are the two arguments we will use to pass in to our connect function. What's great about redux is that this method allows you to abstract out this logic to apply to individual components on a per need basis. For instance, a static page component like your home page or an about page might not necessarily care about having access to states or actions. Imagine on a large scale application with a huge data object tree, you'd only want components that need this data to have access to it. You'd want to avoid triggering re-renders of your components that do not need the state or actions to be passed in.

The mapStateToProps() is essentially passing the entire state tree down to the application as props. The dispatch argument being passed in to mapDispatchToProps is allowing the store to dispatch the actions thats being passed to state which will then later be passed in as props.

We then store our connect function with our new arguments in CounterWrapper. CounterWrapper in this case is a Higher-Order Component, you can read more about it here.

Let's now go back to our template and add in our count value and actions to it's appropriate elements.

class Counter extends Component {
  render() {
  const { increment, decrement, count } = this.props
    return (
      <main className="Counter">
    <p className="count">{count}</p>
    <section className="controls">
      <button onClick={increment}>Increment</button>
      <button onClick={increment}>Decrement</button>
      <button>Reset</button>
    </section>
      </main>
    );
  }
}

You might have remembered that another redux method bindActionCreators is a commonly used one. It does exactly what the name describes, and that is it simply binds action creators together for modularity. We can simply refactor our component by doing this:

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

What we're doing here is we're binding both actions that we created, incrementValue and decrementValue and binding them to the dispatch. This is exactly why Redux is so handy as there is the flexibility to create functions and customize which data you want to be passed in to which component.

Below is the complete file with all the moving parts. And again please note that in a real-world application, it's best to apply Redux's rules and structure in your file system.

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

const initialState = {
    count: 0,
}

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

// These are action creators
const increment = () => ({
    type: INCREMENT,
});

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

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

const store = createStore(reducer);

class Counter extends Component {
  render() {
  const { increment, decrement, count } = this.props
    return (
      <main className="Counter">
    <p className="count">{count}</p>
    <section className="controls">
      <button onClick={increment}>Increment</button>
      <button onClick={increment}>Decrement</button>
      <button>Reset</button>
    </section>
      </main>
    );
  }
}

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

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

const CounterWrapper = connect(mapStateToProps, mapDispatchToProps)(Counter)

render(
    <Provider store={store}>
        <CounterWrapper />
    </Provider>,
    document.getElementById('root')
)

Please feel free to comment below with feedback if you think this tutorial could be improved!

Credits to Steve Kinney in FEM

Discussion (0)