Full Stack Developer, Tech Geek, Audiophile, Cinephile, and Lifelong Learner!

Crud Using React Hooks and Context API

C

Crud Using React Hooks and Context Api | Mushfiqur’s Blog

Crud Using React Hooks and Context Api | Mushfiqur’s Blog

React Hooks and Context API are two relatively new features that have been added to React from v16.8 and v16.3 respectively. In this post, I’m going to create an app that can perform CREATE, READ, UPDATE and DELETE utilizing Hooks and Context API together.

Crud Using React Hooks and Context Api

React Hooks and Context API are two relatively new features that have been added to React from v16.8 and v16.3 respectively. In this post, I’m going to create an app that can perform CREATE, READ, UPDATE and DELETE utilizing Hooks and Context API together.

React Hooks

This new concept was introduced in v16.8 which is an alternative to classes. The Devs who used React before are familiar with functional components and class components. Many of these features available to classes – such as lifecycle methods and state weren’t available to React until Hooks were introduced. The new Hooks add those class component’s features to functional components. Let’s see an example of functional component and class component.

Functional Components

const ExampleComponent = () => {
  return <div>I'm a simple functional component</div>
}

Class Components

class ExampleComponent extends Component {
  render() {
    return <div>I'm a class component</div>
  }
}

React context api

The inception of the Context API resolves one of the most talked issues of React – prop drilling which was introduced in v16.3. This is a process of manipulating data from one component to another through layers of nested components.

Now it’s the time to start coding.

Please to be noted, I’m going to use Tailwind CSS to style our app. Let’s bootstrap our project using Create-React-App with the following command:

npx create-react-app hooks_and_context

Make sure you have the latest Node version is installed. This will create a folder hooks_and_context and bootstrap our project. If we have a close look at the package.json and we will see the following:

{
  "name": "hooks_and_context",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Before going into more coding thing we are going to enhance our development environment by installing a few packages.

ESLInt and prettier

It’s the time we are going to add ESLint and Prettier to make our development environment more friendly. ESLint is a JavaScript linter that will help us find syntax or other errors while we do our code. The ESLint package can be extended by plugging in pre-defined configs or we can completely configure it by ourselves. Regardless of the OS, I’ll recommend anyone to use VSCode as editor. Going forward I’ll be assuming we are using VSCode.

install vscode plugins

First of all, we need to install ESLint and Prettier – Code formatter plugins for VSCode. And we need to make sure they are enabled.

Now, we need to install the required dependencies for ESLint and Prettier into our project. To do so, please run the following command into the project root:

npm install eslint-config-prettier eslint-plugin-prettier prettier --save

A point to be noted here, we are not going to add eslint package as it’s already added through Create-React-App.

setup eslint config

Create an .eslintrc file in the project root directory. In the file we are going to tell ESLint to do the following things:

  • Extend from the recommended prettier config
  • Register the Prettier plugin we installed
  • Show Prettier errors as errors
{
    "extends": [
        "plugin:prettier/recommended"
    ],
    "plugins": [
        "prettier"
    ],
    "rules": {
        "prettier/prettier": "error"
    }
}

The ESLint config can contain many more rules. For the time being, let’s keep it as simple as we can so that can grab the basic ideas faster.

SETUP PRETTIER CONFIG

Next, it’s time to create a new file named .prettierrc in the root directory and add the following config.

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false
}

The above setup would giive us the following behaviors in VSCode:

  • Indent the code with 2 spaces
  • Use of single quotes instead of double
  • Add semicolons at the end of each line

Try restaring VSCode if it doesn’t work as expected. And we are so done with our environment enhancement.

Add React Router

We need to install Tailwind CSS and React Router by running the following command.

npm install react-router-dom tailwindcss --save

Now it’s time to dive into the coding part. First of all, create a folder named components. Create the following files into that folder:

  • Home.js
  • AddEmployees.js
  • EditEmployees.js
  • EmployeeList.js
  • Header.js

Going forward, we are going to import these main components into our App component. To add routing support to our app, we need to add Route and Switch from react-router-dom. Before doing that, let’s wrap our app with GlobalProvider which we need from GlobalState(we are going to define this later). Now the App.js file should look exactly like the following:

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;

Add Tailwind CSS Classes

Now it’s time to go forward to show the list of Employees inside the EmployeeList.js file. The following is the file and we have used Tailwind CSS utility classes and the help style our app so easily.

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>
  );
};

Add Global Context

In the above code, we have imported GlobalState and useContext, one of the built-in React Hooks, giving functional components easy access to the global context. Now we are going to import employees, removeEmployee and editEmployees from Our GlobalState.js file. Let’s move on creating a GlobalState.js file where we will male our function inside of which we will dispatch our actions. Finally, the GlobalState.js should look something like the following:

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

const initialState = {
    employees: [
        { id: 1, name: 'Mushfiqur Rahman', location: 'Dhaka', designation: 'Frontend Dev' }
    ]
}

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.

Add App Reducer

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 setNamesetLocation 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 file App.js where we are going to keep all our routes to the route parameter. Let’s take a look at the 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>
  );
};

Utilize UseEffect

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.

bonus

dockerize react app

To install docker on Ubuntu see this.

Assuming we are on Linux, we need to create a file named Dockerfile(without any extension). To do so type the following in the terminal in the root directory of our app:

touch Dockerfile

Add the following content to the file:

# pull official base image
FROM node:alpine

# set working directory
WORKDIR '/app'

# install all the dependencies
COPY package.json .
RUN npm install

# add app
COPY . .

# start app
CMD ["npm", "start"]

To build the docker image run the following:

docker build . -t reactdoc

The above will create the docker container image. Now run the following command to see if it is actually created:

docker image ls

Above command should show a list of docker images on your system. And you should find reactdoc in the list too.

It’s time to run the docker image:

docker run reactdoc

Open another terminal instance and run docker ps. It will list the running containers with their IDs. Now copy the container Id(cfef6147e1d6 is my container id) and run the following command with it:

docker exec -it cfef6147e1d6 bash

It will let you enter the docker container!

The complete source code is available on github.

Windows Sandbox

Share on:

1 Comment

  |     |  
Full Stack Developer, Tech Geek, Audiophile, Cinephile, and Lifelong Learner!