Tutorial

Building a CRUD App with React Hooks & the Context API

React

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

This article will be covering React hooks, introduced in version 16.8 and the Context API, introduced in 16.3 and mixing them together to build a fully functional CRUD application. Here we won’t be using any external API calls but we’ll get our hands dirty with hard-coded objects which will serve as the state.

The introduction of the Context API solves one major problem: prop drilling. The process of getting our data from one component to another through layers of nested deep components. Whereas Hooks helps us, React developers, by allowing us to use a functional rather than class-based components. Where we needed to utilize a lifecycle method, we had to use a class-based approach. And we now no longer have to call super(props) or worry about binding methods or the this keyword.

Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.

– John Carmack. Oculus VR CTO


Less talk more code. Let’s start coding 🐊🐊🐊

As you’ll notice, we’ll be making use of Tailwind CSS for the styling of our app.

Firstly we’ll start with setting up our React project using Create React App with the following command:

$ npx create-react-app react-contextAPI

We’ll now initialize our package.json file with yarn init - and make sure we have the following dependencies below:

"react": "^16.13.0", 
"react-dom": "^16.13.0", 
"react-router-dom": "^5.1.2", 
"react-scripts": "3.4.0", 
"eslint": "^6.8.0", 
"tailwindcss": "^1.2.0"

To add react-router-dom and Tailwind CSS as dependencies, just run the following command:

$ npm i react-router-dom tailwindcss

And now here are some components we’ll create in a components directory:

  • Home.js
  • AddEmployees.js
  • EditEmployees.js
  • EmployeeList.js
  • Heading.js

Import these main components inside your App component. We’ll also have to import Route and Switch from react-router-dom. And beforehand we will wrap our app with GlobalProvider which we need from GlobalState (which we’ll define later).

Here’s a quick intro to React Router if this is all new to you.

Our App.js file will look something like this:

import React from 'react';
import { Route, Switch } from 'react-router-dom';
import './stylesheet/styles.css';
import { Home } from './components/Home';
import { AddEmployee } from './components/Addemployee';
import { EditEmployee } from './components/Editemployee';


import { GlobalProvider } from './context/GlobalState';

function App() {
  return (
    <GlobalProvider>
      <Switch>
        <Route path="/" component={Home} exact />
        <Route path="/add" component={Addemployee} exact />
        <Route path="/edit/:id" component={Editemployee} exact />
      </Switch>
    </GlobalProvider>
  );
}

export default App;

We will now move forward with printing the list of Employees inside our EmployeeList.js file. The classNames that you’ll notice are Tailwind CSS utility classes and they help style our app.

EmployeeList.js
import React, { Fragment, useContext } from "react";
import { GlobalContext } from "../context/GlobalState";
import { Link } from "react-router-dom";

export const Employeelist = () => {
  const { employees, removeEmployee, editEmployee } = useContext(GlobalContext);
  return (
    <Fragment>
      {employees.length > 0 ? (
        <Fragment>
          {employees.map(employee => (
            <div
              className="flex items-center bg-gray-100 mb-10 shadow"
              key={employee.id}
            >
              <div className="flex-auto text-left px-4 py-2 m-2">
                <p className="text-gray-900 leading-none">{employee.name}</p>
                <p className="text-gray-600">{employee.designation}</p>
                <span className="inline-block text-sm font-semibold mt-1">
                  {employee.location}
                </span>
              </div>
              <div className="flex-auto text-right px-4 py-2 m-2">
                <Link to={`/edit/${employee.id}`}>
                  <button
                    onClick={() => editEmployee(employee.id)}
                    className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold mr-3 py-2 px-4 rounded-full inline-flex items-center"
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="24"
                      height="24"
                      viewBox="0 0 24 24"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="2"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      className="feather feather-edit"
                    >
                      <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
                      <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
                    </svg>
                  </button>
                </Link>
                <button
                  onClick={() => removeEmployee(employee.id)}
                  className="block bg-gray-300 hover:bg-gray-400 text-gray-800 font-semibold py-2 px-4 rounded-full inline-flex items-center"
                >
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="24"
                    height="24"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="feather feather-trash-2"
                  >
                    <polyline points="3 6 5 6 21 6"></polyline>
                    <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
                    <line x1="10" y1="11" x2="10" y2="17"></line>
                    <line x1="14" y1="11" x2="14" y2="17"></line>
                  </svg>
                </button>
              </div>
            </div>
          ))}
        </Fragment>
      ) : (
        <p className="text-center bg-gray-100 text-gray-500 py-5">No data</p>
      )}
    </Fragment>
  );
};

In the above code we imported GlobalState and useContext, one of the built-in React Hooks, giving functional components easy access to our context.

Moreover, we imported our employees object, removeEmployee and editEmployees from our GlobalState.js file.

Let’s move on creating a GlobalState file where we will make our function inside of which w’ll dispatch our action.

import React, { createContext, useReducer } from "react";
import AppReducer from "./AppReducer";

const initialState = {
  employees: [
    {
      id: 1,
      name: "Ishan Manandhar",
      location: "Kathmandu",
      designation: "Frontend Developer"
    }
  ]
};

export const GlobalContext = createContext(initialState);
export const GlobalProvider = ({ children }) => {
  const [state, dispatch] = useReducer(AppReducer, initialState);

  function removeEmployee(id) {
    dispatch({
      type: "REMOVE_EMPLOYEE",
      payload: id
    });
  }

  function addEmployee(employees) {
    dispatch({
      type: "ADD_EMPLOYEES",
      payload: employees
    });
  }

  function editEmployee(employees) {
    dispatch({
      type: "EDIT_EMPLOYEE",
      payload: employees
    });
  }

  return (
    <GlobalContext.Provider
      value={{
        employees: state.employees,
        removeEmployee,
        addEmployee,
        editEmployee
      }}
    >
      {children}
    </GlobalContext.Provider>
  );
};

We added some functionality to dispatch an action which goes into our reducer file to switch upon the case that corresponds to each action.

We also defined the initial state of our employee array with hard-coded values inside the object. Along with the dispatch type we will also add what payload it receives. Let’s move on to our AppReducer file and write some switch cases for CRUD functionality, 🤓 which looks like this:

export default (state, action) => {
  switch (action.type) {
    case "REMOVE_EMPLOYEE":
      return {
        ...state,
        employees: state.employees.filter(
          employee => employee.id !== action.payload
        )
      };
    case "ADD_EMPLOYEES":
      return {
        ...state,
        employees: [...state.employees, action.payload]
      };
    case "EDIT_EMPLOYEE":
      const updatedEmployee = action.payload;

      const updatedEmployees = state.employees.map(employee => {
        if (employee.id === updatedEmployee.id) {
          return updatedEmployee;
        }
        return employee;
      });

      return {
        ...state,
        employees: updatedEmployees
      };
    default:
      return state;
  }
};

In our AppReducer.js file, we added our switch case and wrote some functionality for each case and returned employee state inside respective functions. We’ll move ahead with our AddEmployee component and write an onSubmit handler which will push the filled values of our form field into the state.

Below is how our code looks like:

import React, { Fragment, useState, useContext } from "react";
import { GlobalContext } from "../context/GlobalState";
import { useHistory } from "react-router-dom";
import { Link } from "react-router-dom";

export const AddEmployee = () => {
  const [name, setName] = useState("");
  const [location, setLocation] = useState("");
  const [designation, setDesignation] = useState("");
  const { addEmployee, employees } = useContext(GlobalContext);
  let history = useHistory();

  const onSubmit = e => {
    e.preventDefault();
    const newEmployee = {
      id: employees.length + 1,
      name,
      location,
      designation
    };
    addEmployee(newEmployee);
    history.push("/");
  };

  return (
    <Fragment>
      <div className="w-full max-w-sm container mt-20 mx-auto">
        <form onSubmit={onSubmit}>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="name"
            >
              Name of employee
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
              value={name}
              onChange={e => setName(e.target.value)}
              type="text"
              placeholder="Enter name"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="location"
            >
              Location
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={location}
              onChange={e => setLocation(e.target.value)}
              type="text"
              placeholder="Enter location"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="designation"
            >
              Designation
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:text-gray-600"
              value={designation}
              onChange={e => setDesignation(e.target.value)}
              type="text"
              placeholder="Enter designation"
            />
          </div>
          <div className="flex items-center justify-between">
            <button className="mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
              Add Employee
            </button>
          </div>
          <div className="text-center mt-4 text-gray-500">
            <Link to="/">Cancel</Link>
          </div>
        </form>
      </div>
    </Fragment>
  );
};

Here setName, setLocation and setDesignation will access the current value we typed inside our form fields and wrap it in a new constant, newEmployee, with unique id adding one to the total length. We’ll add a parameter to our GlobalContext we imported and newEmployees as out parameter as it accepts employees as a payload inside our GlobalState. Finally we’ll change our route to our main screen where we’ll be able to see the newly added employees.

We’ll go into our EditEmployee component and write some functionality for editing the existing objects from the state. If you have noticed we added:

path=“/edit/:id”

To our App.js file where we shall route to the route parameter. Lets take a look at following code

import React, { Fragment, useState, useContext, useEffect } from "react";
import { GlobalContext } from "../context/GlobalState";
import { useHistory, Link } from "react-router-dom";

export const Editemployee = route => {
  let history = useHistory();
  const { employees, editEmployee } = useContext(GlobalContext);
  const [selectedUser, setSeletedUser] = useState({
    id: null,
    name: "",
    designation: "",
    location: ""
  });
  const currentUserId = route.match.params.id;

  useEffect(() => {
    const employeeId = currentUserId;
    const selectedUser = employees.find(emp => emp.id === parseInt(employeeId));
    setSeletedUser(selectedUser);
  }, []);

  const onSubmit = e => {
    e.preventDefault();
    editEmployee(selectedUser);
    history.push("/");
  };

  const handleOnChange = (userKey, value) =>
    setSeletedUser({ ...selectedUser, [userKey]: value });

  if (!selectedUser || !selectedUser.id) {
    alert("Id dont match !");
  }

  return (
    <Fragment>
      <div className="w-full max-w-sm container mt-20 mx-auto">
        <form onSubmit={onSubmit}>
          <div className="w-full mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="name"
            >
              Name of employee
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.name}
              onChange={e => handleOnChange("name", e.target.value)}
              type="text"
              placeholder="Enter name"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="location"
            >
              Location
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.location}
              onChange={e => handleOnChange("location", e.target.value)}
              type="text"
              placeholder="Enter location"
            />
          </div>
          <div className="w-full  mb-5">
            <label
              className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
              htmlFor="designation"
            >
              Designation
            </label>
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:text-gray-600 focus:shadow-outline"
              value={selectedUser.designation}
              onChange={e => handleOnChange("designation", e.target.value)}
              type="text"
              placeholder="Enter designation"
            />
          </div>
          <div className="flex items-center justify-between">
            <button className="block mt-5 bg-green-400 w-full hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:text-gray-600 focus:shadow-outline">
              Edit Employee
            </button>
          </div>
          <div className="text-center mt-4 text-gray-500">
            <Link to="/">Cancel</Link>
          </div>
        </form>
      </div>
    </Fragment>
  );
};

Here we used the useEffect hook, which is invoked when the component is mounted. 💣 Inside this hook we’ll know the current route parameter and find the same parameter to our employees object from the state. We then created the setSelectedUser function and passed selectedUser as its parameter. We then observe for onChange events on our form fields where we pass on userKey and value as two parameters. We spread selectedUser and set userKey as value from the input fields.

Finally invoking the onSubmit event works just fine. 🌈🌈 Here we have successfully created our CRUD application using the Context API and hooks.

You can also find the code in a GitHub repository here.

Happy Coding!

Creative Commons License