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 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 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.
there is no update method here