A Test Driven Approach with React/Redux

Introduction

One of the key considerations when building a React application is how to deal with state management. React gives us the ability to create modular components and seamlessly integrate our data into the component flow; however, data flow through a set of components can become unruly quickly. While utilizing React context provides a viable solution by eliminating the need to pass data through the component tree manually (prop-drilling), Redux offers an elegant solution by utilizing a central store to hold the entire state of the application. As always, click the Github link in the corner to get the source code for this project!

In this article, we will be building a grocery list application that will demonstrate the different features that Redux has to offer. But first, let’s work through some of the basics.

How does Redux Work?

As stated above, Redux provides a central store that contains our applications state. This is all well and good, but data changes and we need to be able to modify our state. To do this, there are two key functions provided that allow us to modify the stored data: actions and reducers.

Actions

Actions allow us to communicate with our Redux data store. You can think of them as events They are plain old JavaScript objects that are the only allowable source of information for the data store. Actions must have a type property that indicates the action being performed. This will make more sense as we get into our project.

Reducers

Reducers work directly on the applications state. They are pure functions that respond to actions sent to the store and return the new state after performing the requested action. Redux reducers are essentially an implementation of the reduce function in JavaScript.

Setting up our Application

I will be using Yarn for this application; however, feel free to use NPM if you feel more comfortable doing it that way. Inside of your project directory you’ll want to run the following:

yarn create react-app client

We’ll also be utilizing Semantic UI for our buttons and initial UI layout. You can add that directly to the index.html located in public/.

Now remove all of the files and folders inside of the src/ directory. We’ll be creating everything from scratch for simplicity’s sake.

Now change directories into the src/ directory and run:

mkdir __mocks__ actions components reducers tests && touch index.js

If you’ve done everything correctly, your directory structure should look the same as:

We will be using a few different libraries throughout this project. Mostly importantly, we need to include React-redux, Redux and Redux-Thunk.

Redux-Thunk is a piece of middleware that allows us to write asynchronous logic. In our case, we will be using it to interact with our simple REST API via Axios. Anyway, let’s get these dependencies installed! Make sure you are in the client/ directory and run the following:

yarn add axios lodash react-redux redux redux-thunk styled-components

Also, since we’ll be building our project from our initial tests, you’ll want to install jest as well.

yarn add -D jest

Great! Now we have our client dependencies and folder structure created. Let’s move on and create our simple REST API.

Building our API

In the root of the project directory, create a directory named api if you haven’t already.

mkdir api && cd api/

Now, we’ll want to create a new Yarn project by running:

yarn init

You can fill our the information you want, I just left the details blank for now. We’ll be using a simple REST server called JSON-Server for this project, so add that dependency:

yarn add json-server

Open the package.json file and add the script as shown in the picture below:

Alright, now we’ll just need to add the db.json file to the directory and add in some dummy data.

touch db.json

Add the following dummy data to the file and that should do it for our API setup!

Alright! Our initial setup is complete and we can move on to building out the application.

Adding Actions to our Application

Our application is going to have four basic actions. We want to be able to:

  • Fetch our grocery list from our API
  • Add grocery items to the list
  • Edit existing grocery items in case of any mistakes
  • Delete existing grocery items as we place them in our cart

Navigate to client/src/actions and create a file named index.js. This will be where all of our actions live. Modify the file to look like the image below:

You may notice that we have functions that return functions. This is where Redux-Thunk comes into play. Since Redux store only supports synchronous data flow, we need to incorporate middleware that allows us to make AJAX calls.

Creating Reducers for State Mangement

As stated above, our grocery list application will have 4 basic functions. The actions will take care of interacting with the REST API; however, we need to implement reducers that respond to the payloads that are sent from the actions. Thus, we will need a reducer to match each of the 4 cases.

Navigate to client/src/reducers and create two new files: groceryReducer.js and index.js. Inside of groceryReducer.js, add the code from the following image.

And just to make things a little bit more verbose, we’ll add the following to client/src/reducers/index.js.

In this case, I’m demonstrating that you can use Redux’s built-in function to compose multiple reducers.

We now have our data flow! We can call our actions which will interact with our REST API, append the response to the payload and send it over to the reducer to work with the state. Let’s write some tests and make sure that all of this is working as expecting!

Test…Test…Test

Before we begin creating the UI, let’s make sure that our data does what we expect. In this section, we will create a mock instance of Axios to mimic its get, post, patch and delete methods (so that we don’t modify our database directly) and test all of our actions and reducers to ensure our data flow works as expected.

Let’s get started by creating our mock instance of Axios. Change directories to client/src/__mocks__ and create a new file named axios.js. Complete the text as shown below:

Essentially, we are creating a mock API in this file. We are telling Jest to use our versions of get, post, patch and delete and we are also maintaining state to act as our database. This will allow us to execute our tests without modifying the database directly.

Adding our Test Suite

Change directories to src/tests and create a file named grocery.spec.js. Each test will dispatch an event. In our case, it will reference our action creators and compare our expected result to the state that is returned after being transformed by our reducers. Once all of your tests are passing, I think it’s time we move on to creating our UI.

import { applyMiddleware, createStore } from "redux";
import reduxThunk from "redux-thunk";
import groceryReducer from "../reducers/groceryReducer";

import { addGroceryItem, editGroceryItem, fetchGroceries } from "../actions";

const store = createStore(groceryReducer, applyMiddleware(reduxThunk));

describe("Test suite for Groceries", () => {
  test("Fetch grocery list", async () => {
    await store.dispatch(fetchGroceries());

    expect(store.getState()).toEqual({
      "1": {
        name: "Chicken",
        count: "2 lbs",
        aisle: 14,
        showEdit: false,
        id: 1
      }
    });
  });
  test("Adds item to grocery list", async () => {
    await store.dispatch(
      addGroceryItem({
        name: "",
        count: "",
        aisle: "",
        id: 2,
        showEdit: true
      })
    );
    expect(store.getState()).toEqual({
      "1": {
        name: "Chicken",
        count: "2 lbs",
        aisle: 14,
        showEdit: false,
        id: 1
      },
      "2": {
        aisle: "",
        count: "",
        id: 2,
        name: "",
        showEdit: true
      }
    });
  });
  test("Edit existing item in grocery list", async () => {
    await store.dispatch(
      editGroceryItem("2", {
        aisle: 14,
        count: "1",
        id: 2,
        name: "Pizza",
        showEdit: false
      })
    );
    expect(store.getState()).toEqual({
      "1": {
        name: "Chicken",
        count: "2 lbs",
        aisle: 14,
        showEdit: false,
        id: 1
      },
      "2": {
        aisle: 14,
        count: "1",
        id: 2,
        name: "Pizza",
        showEdit: false
      }
    });
  });
  test("Remove existing item fro grocery list", () =>
    expect(true).toEqual(true));
});

Creating our User Interface

The time has come! All of our tests are passing and the data flow is working as expected. We need a way of displaying this data that allows the user to view, add and remove groceries from their list.

We’ll start by changing directories to client/src/index.js and adding the following:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import reduxThunk from "redux-thunk";

import App from "./components/App";
import reducers from "./reducers";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  reducers,
  composeEnhancers(applyMiddleware(reduxThunk))
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector("#root")
);

What is happening here?

As with all React applications, we render to the root element on the DOM; however, in this case, we need to wrap our application with our Redux store. So, here we are:

  1. Creating the Redux store and adding the reducers that we created as it’s first argument.
  2. Applying middleware to Redux in order to allow us to execute asynchronous functionality and composing that along with Redux’s DevTool extension for development use. Check it out here.
  3. Wrapping our application with Redux’s react provider so that we can utilize the power of Redux in our application!

With our App component wrapped with our data store, we are now able to access the actions and reducers insight our components (with a little configuration of course!).

App Component

The App component is a simple component that will be the home to our layout and container our GroceryList component:

import React from "react";
import styled from "styled-components";
import Header from "./Header";
import GroceryList from "./GroceryList";

const AppWrapper = styled.div`
  .navbar {
    background-color: black;
    padding: 10px;
    margin: 0;
    color: #fff;
  }
`;

const App = () => {
  return (
    <AppWrapper>
      <div className="navbar">
        <Header />
      </div>
      <div className="ui container">
        <GroceryList />
      </div>
    </AppWrapper>
  );
};

export default App;

GroceryList Component – Plugging Redux into our User Interface

In order to connect our store to a specific component, we’ll need to reference the connect function that React Redux provides us with. The syntax is a bit on the wonky side; however, the functionality is actually pretty simple. In the case of our GroceryList component, we are providing two arguments to the first set of parameters. The first is a function expression called mapStateToProps. This function allows us to take the state of our redux store and map it to the props passed into the component (the argument to the second set of parenthesis). The second argument allows us to map any dispatchers that we’ll be using to manipulate the store. Again, these are made available to us via props.

import React, { useEffect } from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import { fetchGroceries, addGroceryItem } from "../actions";
import GroceryItem from "./GroceryItem";

const GroceryListWrapper = styled.div`
  .table-heading {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    margin: 1rem 0;
    .grocery-table-heading {
      font-size: 1.6rem;
    }
  }
`;

const GroceryList = props => {
  useEffect(() => {
    props.fetchGroceries();
  }, []);
  return (
    <GroceryListWrapper>
      <div className="table-heading">
        <span className="grocery-table-heading">Item</span>
        <span className="grocery-table-heading">Amount</span>
        <span className="grocery-table-heading">Location (Aisle)</span>
      </div>
      {props.groceries.map(item => (
        <GroceryItem {...item} key={item.id} />
      ))}
      <button
        className="ui green button"
        onClick={() =>
          props.addGroceryItem({
            name: "",
            count: "",
            aisle: "",
            showEdit: true
          })
        }
      >
        Add Item
      </button>
    </GroceryListWrapper>
  );
};

const mapStateToProps = state => {
  return {
    groceries: Object.values(state.groceries)
  };
};

export default connect(mapStateToProps, { fetchGroceries, addGroceryItem })(
  GroceryList
);

Alright, now that we have our Redux actions, reducers and state available to us, we can utilize the useEffect hook to call fetchGroceries on component mount. You will need to include the empty array otherwise the fetchGroceries function will be run after every change to props and state!

GroceryItem Component – The visual representation of our State

The last item we need to create is the GroceryItem. As with every component that relies on Redux, we’ll export the connect()() wrapped component with some arguments; however, this time we will not be mapping our Redux state to props. Since we are relying on our GroceryList component to provide us with all of the necessary props, we can provide the connect function with a value of null for the first argument. For our second argument, we’ll want to give our component the ability to dispatch modifications to our individual items. Thus, we’ll provide editGroceryItem and deleteGroceryItem to our component props.

import React, { useState } from "react";
import { connect } from "react-redux";
import { editGroceryItem, deleteGroceryItem } from "../actions";
import styled from "styled-components";

const GroceryItemWrapper = styled.div`
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  font-size: 1rem;
  border-top: 0.1rem solid #cbcbcb;
  .grocery-item-field {
    align-self: center;
    margin-left: 1rem;
  }
`;

const GroceryItem = ({
  id,
  name,
  count,
  aisle,
  showEdit,
  deleteGroceryItem,
  editGroceryItem
}) => {
  const [editMode, setEditMode] = useState(showEdit);
  const [groceryName, setGroceryName] = useState(name);
  const [groceryCount, setGroceryCount] = useState(count);
  const [groceryAisle, setGroceryAisle] = useState(aisle);

  const GroceryEdit = () => (
    <>
      <input
        type="text"
        value={groceryName}
        onChange={e => setGroceryName(e.target.value)}
      />
      <input
        type="text"
        value={groceryCount}
        onChange={e => setGroceryCount(e.target.value)}
      />
      <input
        type="text"
        value={groceryAisle}
        onChange={e => setGroceryAisle(e.target.value)}
      />
      <span className="grocery-list-modifiers">
        <button
          className="ui primary button"
          onClick={() => {
            setEditMode(false);
            editGroceryItem(id, {
              name: groceryName,
              count: groceryCount,
              aisle: groceryAisle,
              showEdit: false
            });
          }}
        >
          Save
        </button>
        <button
          className="ui button"
          onClick={() => {
            setEditMode(false);
          }}
        >
          Cancel
        </button>
      </span>
    </>
  );

  const GroceryDisplay = () => (
    <>
      <span className="grocery-item-field">{name}</span>
      <span className="grocery-item-field">{count}</span>
      <span className="grocery-item-field">{aisle}</span>
      <span className="grocery-list-modifiers">
        <button className="ui green button" onClick={() => setEditMode(true)}>
          Edit
        </button>
        <button className="ui red button" onClick={() => deleteGroceryItem(id)}>
          Remove
        </button>
      </span>
    </>
  );

  return (
    <GroceryItemWrapper>
      {editMode ? GroceryEdit() : GroceryDisplay()}
    </GroceryItemWrapper>
  );
};

export default connect(null, { editGroceryItem, deleteGroceryItem })(
  GroceryItem
);

The GroceryItem component utilizes React’s useState to give us the ability to manipulate the data provided. Note, useState will not be manipulating the store data directly. We rely on our dispatch functions to do that. Since we rely on Redux to abstract away the details of modifying our data store, we don’t need to worry about unexpected state mutation affecting the integrity of our application or coupling our UI to tightly to our data!

Conclusion….

Thanks for coming along on the journey of learning Redux. I encourage you to check out the documentation to get a deeper understanding. Also, check out Dan Abramov’s Redux course on egghead.io. He is the creator of Redux and he happens to be a very good teacher. Anyway, thanks again and hopefully this was a useful tutorial for you.