How to Replace Redux with React Hooks and the Context API

The most popular way for handling shared application state in React is using a framework such as Redux. Quite recently, the React team introduced several new features which include React Hooks and the Context API. These two features effectively eliminated a lot of challenges that developers of large React projects have been facing. One of the biggest problems was ‘prop drilling’ which was common with nested components. The solution was to use a state management library like Redux. This unfortunately came with the expense of writing boilerplate code — but now, it’s possible to replace Redux with React Hooks and the Context API.

In this article, you are going to learn a new way of handling state in your React projects without writing excessive code or installing a bunch of libraries as is the case with Redux. React hooks allows you to use local state inside of function components, while the Context API allows you to share state with other components.

Prerequisites

In order to follow along with this tutorial, you will need to have a good foundation in the following topics:

The technique you will learn here is based on patterns that were introduced in Redux. This means you need to have a firm understanding of reducers and actions before proceeding. I am currently using Visual Studio Code, which seems to be the most popular code editor right now (especially for JavaScript developers). If you are on Windows, I would recommend you install Git Bash. Use the Git Bash terminal to perform all commands provided in this tutorial. Cmder is also a good terminal capable of executing most Linux commands on Windows.

You can access the complete project used in this tutorial from this GitHub Repository.

About the New State Management Technique

There are two types of state that we need to deal with in React projects:

  • local state
  • global state

Local states can only be used within the components that were created. Global states can be shared across several components. Either way, they are two ways of declaring and handling state using React hooks:

  • useState
  • useReducer

useState is recommended for handling simple values like numbers or strings. However, when it comes to handling complex data structures, you will need to use useReducer. Unlike useState that only comes with a setValue function, the useReducer hook allows you to specify as many functions as you need. For example, an object array state will need at least functions for adding, updating and deleting an item.

Once you declare your state using either useState or useReducer, you can lift it up to global using React Context. This is the technology that will allow you to share values between components without having to pass down props. When you declare a Context Object, it serves as Provider for other components to consume and subscribe to context changes. You can add as many component consumers as you want to the provider. The shared state will sync up automatically with all subscribed components.

Let’s start creating the project so that you can have practical knowledge of how it all works.

Setting Up the Project

The easiest way to create a project is to use create-react-app tool. However, the tool does install a ton of development dependencies that consume a lot of disk space. As a result, it takes a longer time to install and a longer time to spin up the dev server. If you don’t mind the minor issues, you can go ahead a create a new React project with the tool. You can call it react-hooks-context-demo.

Another way of creating a new React project is by cloning a starter project configured to use Parcel JS as the builder. This method consumes at least 50% less disk space and starts the dev server faster than the create-react-app tool. I’ve created one specifically for React tutorials such as this one. I would recommend that you first create a completely blank GitHub repository on your account, before you proceed with these instructions:

$ git clone git@github.com:brandiqa/react-parcel-starter.git react-hooks-context-demo
$ cd react-hooks-context-demo
$ git remote rm origin
# Replace `username` and `repositoryName` with yours
$ git remote add origin git@github.com:username/repositoryName.git
$ git config master.remote origin
$ git config master.merge refs/heads/master
$ git push -u origin master
# Install dependencies
$ npm install

After you have completed executing all the above instructions, you can use the command npm start to start the dev server. You’ll need to launch your browser and navigate to the page localhost:1234.

01-react-parcel-starter

If you used the create-react-app tool, it will of course look different. That is okay since we’ll change the default view in the next step. If your project has started up fine, you can move on to the next section.

Installing a User Interface Library

This step is not necessary for this topic. However, I always like building clean and beautiful interfaces with the least amount of effort. For this tutorial, we’ll use Semantic UI React. Since this is a tutorial about state management, I won’t explain how the library works. I’ll only show you how to use it.

npm install semantic-ui-react semantic-ui-css

Open index.js and insert the following imports:

import 'semantic-ui-css/semantic.min.css';
import './index.css';

That’s all we need to do for our project to start using Semantic UI. Let’s start working on the first example demonstrating this new state management technique.

Counter Example

In this example, we’ll build a simple counter demo consisting of two buttons and a display button. In order to demonstrate global state, this example will be made up of two presentational components. First, we’ll need to define our context object where the state will live. It is similar to store in Redux. Creating our context code to be used for this purpose will require a bit of boilerplate code that will need to be duplicated in every project. Luckily, someone has already written a custom hook for this which will allow you to create your context object in a single line. Simply install the constate package:

npm install constate

With that installed, you should be able to proceed. I’ve placed comments in the code to explain what is happening. Create the store context object file context/CounterContext.js and insert this code:

import { useState } from "react";
import createUseContext from "constate"; // State Context Object Creator

// Step 1: Create a custom hook that contains your state and actions
function useCounter() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);
  return { count, increment, decrement };
}

// Step 2: Declare your context state object to share the state with other components
export const useCounterContext = createUseContext(useCounter);

Create the parent component views/Counter.jsx and insert this code:

import React from "react";
import { Segment } from "semantic-ui-react";

import CounterDisplay from "../components/CounterDisplay";
import CounterButtons from "../components/CounterButtons";
import { useCounterContext } from "../context/CounterContext";

export default function Counter() {
  return (
    // Step 3: Wrap the components you want to share state with using the context provider
    <useCounterContext.Provider>
      <h3>Counter</h3>
      <Segment textAlign="center">
        <CounterDisplay />
        <CounterButtons />
      </Segment>
    </useCounterContext.Provider>
  );
}

Create the presentation component components/CounterDisplay.jsx and insert this code:

import React from "react";
import { Statistic } from "semantic-ui-react";
import { useCounterContext } from "../context/CounterContext";

export default function CounterDisplay() {
  // Step 4: Consume the context to access the shared state
  const { count } = useCounterContext();
  return (
    <Statistic>
      <Statistic.Value>{count}</Statistic.Value>
      <Statistic.Label>Counter</Statistic.Label>
    </Statistic>
  );
}

Create the presentation component components/CounterButtons.jsx and insert this code:

import React from "react";
import { Button } from "semantic-ui-react";
import { useCounterContext } from "../context/CounterContext";

export default function CounterButtons() {
  // Step 4: Consume the context to access the shared actions
  const { increment, decrement } = useCounterContext();
  return (
    <div>
      <Button.Group>
        <Button color="green" onClick={increment}>
          Add
        </Button>
        <Button color="red" onClick={decrement}>
          Minus
        </Button>
      </Button.Group>
    </div>
  );
}

Replace the code in App.jsx with this:

import React from "react";
import { Container } from "semantic-ui-react";

import Counter from "./views/Counter";

export default function App() {
  return (
    <Container>
      <h1>React Hooks Context Demo</h1>
      <Counter />
    </Container>
  );
}

Your browser page should have the following view. Click the buttons to ensure that everything is working:

02-counter-demo.

Hope this example makes sense — read the comments I’ve included. Let’s go to the next section where we’ll set up an example that is a bit more advanced.

Contacts Example

In this example, we’ll build a basic CRUD page for managing contacts. It will be made up of a couple of presentational components and a container. There will also be a context object for managing contacts state. Since our state tree will be a bit more complex than the previous example, we will have to use the useReducer hook.

Create the state context object context/ContactContext.js and insert this code:

import { useReducer } from "react";
import _ from "lodash";
import createUseContext from "constate";

// Define the initial state of our app
const initialState = {
  contacts: [
    {
      id: "098",
      name: "Diana Prince",
      email: "diana@us.army.mil"
    },
    {
      id: "099",
      name: "Bruce Wayne",
      email: "bruce@batmail.com"
    },
    {
      id: "100",
      name: "Clark Kent",
      email: "clark@metropolitan.com"
    }
  ],
  loading: false,
  error: null
};

// Define a pure function reducer
const reducer = (state, action) => {
  switch (action.type) {
    case "ADD_CONTACT":
      return {
        contacts: [...state.contacts, action.payload]
      };
    case "DEL_CONTACT":
      return {
        contacts: state.contacts.filter(contact => contact.id != action.payload)
      };
    case "START":
      return {
        loading: true
      };
    case "COMPLETE":
      return {
        loading: false
      };
    default:
      throw new Error();
  }
};

// Define your custom hook that contains your state, dispatcher and actions
const useContacts = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { contacts, loading } = state;
  const addContact = (name, email) => {
    dispatch({
      type: "ADD_CONTACT",
      payload: { id: _.uniqueId(10), name, email }
    });
  };
  const delContact = id => {
    dispatch({
      type: "DEL_CONTACT",
      payload: id
    });
  };
  return { contacts, loading, addContact, delContact };
};

// Share your custom hook
export const useContactsContext = createUseContext(useContacts);

Create the parent component views/Contacts.jsx and insert this code:

import React from "react";
import { Segment, Header } from "semantic-ui-react";
import ContactForm from "../components/ContactForm";
import ContactTable from "../components/ContactTable";
import { useContactsContext } from "../context/ContactContext";

export default function Contacts() {
  return (
    // Wrap the components that you want to share your custom hook state
    <useContactsContext.Provider>
      <Segment basic>
        <Header as="h3">Contacts</Header>
        <ContactForm />
        <ContactTable />
      </Segment>
    </useContactsContext.Provider>
  );
}

Create the presentation component components/ContactTable.jsx and insert this code:

import React, { useState } from "react";
import { Segment, Table, Button, Icon } from "semantic-ui-react";
import { useContactsContext } from "../context/ContactContext";

export default function ContactTable() {
  // Subscribe to `contacts` state and access `delContact` action
  const { contacts, delContact } = useContactsContext();
  // Declare a local state to be used internally by this component
  const [selectedId, setSelectedId] = useState();

  const onRemoveUser = () => {
    delContact(selectedId);
    setSelectedId(null); // Clear selection
  };

  const rows = contacts.map(contact => (
    <Table.Row
      key={contact.id}
      onClick={() => setSelectedId(contact.id)}
      active={contact.id === selectedId}
    >
      <Table.Cell>{contact.id}</Table.Cell>
      <Table.Cell>{contact.name}</Table.Cell>
      <Table.Cell>{contact.email}</Table.Cell>
    </Table.Row>
  ));

  return (
    <Segment>
      <Table celled striped selectable>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell>Id</Table.HeaderCell>
            <Table.HeaderCell>Name</Table.HeaderCell>
            <Table.HeaderCell>Email</Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        <Table.Body>{rows}</Table.Body>
        <Table.Footer fullWidth>
          <Table.Row>
            <Table.HeaderCell />
            <Table.HeaderCell colSpan="4">
              <Button
                floated="right"
                icon
                labelPosition="left"
                color="red"
                size="small"
                disabled={!selectedId}
                onClick={onRemoveUser}
              >
                <Icon name="trash" /> Remove User
              </Button>
            </Table.HeaderCell>
          </Table.Row>
        </Table.Footer>
      </Table>
    </Segment>
  );
}

Create the presentation component components/ContactForm.jsx and insert this code:

import React, { useState } from "react";
import { Segment, Form, Input, Button } from "semantic-ui-react";
import { useContactsContext } from "../context/ContactContext";

export default function ContactForm() {
  const name = useFormInput("");
  const email = useFormInput("");
  // Consume the context store to access the `addContact` action
  const { addContact } = useContactsContext();

  const onSubmit = () => {
    addContact(name.value, email.value);
    // Reset Form
    name.onReset();
    email.onReset();
  };

  return (
    <Segment basic>
      <Form onSubmit={onSubmit}>
        <Form.Group widths="3">
          <Form.Field width={6}>
            <Input placeholder="Enter Name" {...name} required />
          </Form.Field>
          <Form.Field width={6}>
            <Input placeholder="Enter Email" {...email} type="email" required />
          </Form.Field>
          <Form.Field width={4}>
            <Button fluid primary>
              New Contact
            </Button>
          </Form.Field>
        </Form.Group>
      </Form>
    </Segment>
  );
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  function handleReset() {
    setValue("");
  }

  return {
    value,
    onChange: handleChange,
    onReset: handleReset
  };
}

Insert the following code in App.jsx accordingly:

import Contacts from "./views/Contacts";
//...
<Container>
  <h1>React Hooks Context Demo</h1>
  {/* <Counter /> */}
  <Contacts />
</Container>;

After implementing the code, your browser page should refresh. To delete a contact, you need to select a row first then hit the ‘Delete button’. To create a new contact, simply fill the form and hit the ‘New Contact’ button.

03-contacts-example

Go over the code to make sure you understand everything. Read the comments that I’ve included inside the code.

Summary

Hope both these examples provide an excellent understanding of how you can manage shared application state without Redux. If you were to rewrite these examples without hooks and the context API, it would have resulted in a lot more code. You should only use the context API where applicable. Props should have been used in these examples if this wasn’t a tutorial.

You may have noticed in the second example that there are a couple of unused state variables i.e. loading and error. As a challenge, you can progress this app further to make use of them. For example, you can implement a fake delay, and cause the presentation components to display a loading status. You can also take it much further and access a real remote API. This is where the error state variable can come handy in displaying error messages.

The only question you may want to ask yourself now: is Redux necessary for future projects? One disadvantage that I’ve seen with this technique is that you can’t use the Redux DevTool Addon to debug your application state. However, this might change in the future with the development of a new tool. Obviously as a developer, you will still need to learn Redux in order to maintain legacy projects. If you are starting a new project, you will need to ask yourself and your team if using a state management library is really necessary for your case.