Building a CRUD App with React and Local Storage

Arimoro Olamilekan
author
Arimoro Olamilekan
Colorful symbols on a blue background

Introduction

In this tutorial, we will be building a Task Manager App which has CRUD operation. The CRUD will involve creating, reading, updating, and deleting tasks. There will also be an additional dive in to the concept of Local Storage, which we will use to store our data.

Prerequisites

To get the most out of this tutorial, you need to be familiar with JavaScript since React is a JavaScript Framework. I wrote an article on what to learn before a JavaScript Framework like React here. So you can check it out to understand some basic things before going into this tutorial.

Goals

  • You should be able to build any CRUD App like a note-taking app after this tutorial.
  • You should be able to use React Hooks and understand some React concepts like states, props, and components.
  • You should understand the concept of Local Storage and be able to use it in other React apps.

Final View of our App

The final view of our app is displayed below, and this was achieved by styling our app according to the user interface.

You can also view the final project live.

Our App Pre-loader Empty Task Page

Add Task Form Added Task Page

Added Task Alert Delete Task Alert

Project Installation and Set-up

Create React App

In this tutorial, we will start our project using create-react-app

You don't need to install or configure tools like webpack or Babel while using Create React App because they are preconfigured and hidden so that you can focus on the code.

create-react-app also sets up your development environment so that you can use the latest JavaScript features, provides a nice developer experience, and optimizes your app for production.

On your machine you’ll need to have Node >= 10.16 and npm >= 5.6.

To create our new app, you may choose one of the following methods:

  • npx - npx create-react-app task-manager-app

  • npm - npm init react-app task-manager-app

  • yarn - yarn create react-app task-manager-app

In this tutorial, we will use the Yarn method to create our new App, but you can use whichever method you wish.

After the above operation, a new react app will be created called task-manager-app. The structure of the folders is shown below.

task-manager-app ├── README.md ├── node_modules ├── package.json ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── reportWebVitals.js └── setupTests.js

Project Clean-up

After installing our new react app called task-manager-app then it is time to run the project on our browser and clean up unnecessary files in our app so that we can start building The Task Manager App.

To run the installed React App on the same terminal, navigate to the main project folder and run the app by running the following commands in your terminal window.

cd task-manager-app yarn start

This command will start the app on localhost:3000 and open the browser automatically. The landing page on our browser should look like this after the above operation.

Our React Default Page

Now that we have the project running already on port 3000, we can now clean up the project and write our first "Hello World".

Open the App.js file in the src folder and replace all of the code with the code snippet below. Next, open the index.css file and clear all the default styles.

import './App.css';
function App() {
return (
<h1>Hello World</h1>
);
}
export default App;

First Hello World

At this point, we now have our project set up and running on the browser. The next thing to do now is to start building the Task Manager App.

Install Libraries

Install all of the following libraries using npm or yarn. I will be using yarn.

  • yarn add react-icons
  • yarn add sweetalert2
  • yarn add uuid

Bootstrap Framework

The CSS framework that we will use to build this app is Bootstrap, which is very easy to use with React.

In this tutorial, we will be using the CDN method to include Bootstrap in our app.

Firstly you will go to the public folder of our app, within the public folder you'll find an index.html file where all our pages and components are rendered as a single page application. Then in the index.html file, paste the CSS CDN link in the head tag and the JavaScript CDN link in the body tag.

CSS CDN Link

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

JavaScript CDN Link

<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>

After pasting the two CDN Link in the necessary places, the index.html file in the public folder should look like this:

Now that our project is setup let's dive into creating our App.

Creating App Components and Styles

Components in React let you split the UI into independent, reusable pieces and think about each piece in isolation.

In this section, we will create all the components that this app consists of, like the Header, AddTask, Button, Task, and Tasks Component.

Creating Component Folder and File Names

Open the src folder and create a new folder called components, then in the components folder, create the following five files.

  • AddTask.js
  • Button.js
  • Header.js
  • Task.js
  • Tasks.js

After creating the five files and components folder, it should look like the one in the image below.


Components Folder Structure

AddTask Component

The AddTask Component will handle the form that takes in new tasks in our app.

Now let's build this component and create the form. The component will consist of a form along with validation for the form.

AddTask Form

This form consists of three input tags which are used for receiving the task name, the day and time, and for submitting the details received. It also consists of some class names which are used for styling the form and making it look very nice.

This form is handled by two states which help us to get the values of the form. In this case, we would be using our useState React Hook because we are using a Functional Component.

note: useState

It is the most important and often used hook when using Functional components. The purpose of this hook is to handle reactive data. Any data that we keep track of over time in the application is called state. When any of the data changes, React re-renders the UI.

Syntax of useState

const [state, setState] = useState(initialState);

The first thing is to import this react hook(useState) into our AddTask Component.

import { useState } from 'react';

After importing this hook, let's create the form in the functional component. So when we fill the form, it is stored in the text and day state, which are empty by default.

const AddTask = ({onSave}) => {
const [text, setText] = useState('');
const [day, setDay] = useState('');
return (
<form className="add-form" onSubmit={onSubmit}>
<div className="form-control">
<label>Task</label>
<input type="text" placeholder="add task" value={text} onChange={(e) => setText(e.target.value)} />
</div>
<div className="form-control">
<label>Day & Time</label>
<input type="text" placeholder="add day & time" value={day} onChange{(e) => setDay(e.target.value)} />
</div>
<input type="submit" className="btn btn-block" value="Save Task" />
</form>
)
}
export default AddTask

Now that we have created our form let's style it and make it look very nice. Open src/index.css and add our styles there.

.add-form {
margin-bottom: 40px;
}
.form-control {
margin: 20px 0;
}
.form-control label {
display: block;
}
.form-control input {
width: 100%;
height: 40px;
margin: 5px;
padding: 3px 7px;
font-size: 17px;
}
.form-control input:focus {
outline: 0;
}
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
.btn:focus {
outline: none;
color: white;
}
.btn-block {
display: block;
width: 100%;
}

The form is now set for adding tasks. Let's now do a little validation to restrict an empty form submission and fill in the form's data.

Form Validation

Since our form is set, let's now validate it onSubmit, so when a user clicks on the save task button or hits enter, it should check the form data based on some conditions and alert the user if there is an error.

But before we do the validation, we need to import the alert that will notify the user, which in this case it is the sweetalert2 library that we installed.

import Swal from "sweetalert2"

The main logic behind the validation is that when the save task button is clicked, the onSubmit function will be called, and as we can see below that inside the onSubmit function, we used a conditional statem ent to check if thetext and day state are empty and if it is empty we want to throw an alert that says "Fill in your task and date or close the form!".

If the text state is empty and the day is available, we will also throw an alert that says "Fill in your task!"

If the text state is available and the day is empty, we will also throw an alert that says "Fill in your date!"

But if both the text and day state are available, then we can call the onSave function we passed as props from our App component, then the text and day state will be passed as an argument into the function.

Then when our form is submitted, we want to set the two states back to an empty string

Note: In case you don't understand the meaning of props, Props are arguments passed into React components, and they can be simply called properties. The main purpose of props is to pass data from one component to another.

const onSubmit = (e) => {
e.preventDefault();
if (!text && !day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your task and date or close the form!'
})
} else if (!text && day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your task!'
})
} else if (text && !day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your date!'
})
} else {
onSave({ text, day });
}
setText('');
setDay('');
}

At this point of the tutorial, we have successfully created our AddTask Component by creating the AddTask form and validating it based on some conditions stated above.

Your AddTask Component should look like this after following the above process:

import { useState } from 'react';
import Swal from "sweetalert2";
const AddTask = ({ onSave }) => {
const [text, setText] = useState('');
const [day, setDay] = useState('');
const onSubmit = (e) => {
e.preventDefault();
if (!text && !day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your task and date or close the form!'
})
} else if (!text && day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your task!'
})
} else if (text && !day) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: 'Fill in your date!'
})
} else {
onSave({ text, day });
}
setText('');
setDay('');
}
return (
<form className="add-form" onSubmit={onSubmit}>
<div className="form-control">
<label>Task</label>
<input type="text" placeholder="add task" value={text} onChange={(e) => setText(e.target.value)} />
</div>
<div className="form-control">
<label>Day & Time</label>
<input type="text" placeholder="add day & time" value={day} onChange={(e) => setDay(e.target.value)} />
</div>
<input type="submit" className="btn btn-block" value="Save Task" />
</form>
)
}
export default AddTask;

Button Component

The Button Component is not very large, but the reason for creating it as a component is so that it can be re-used anywhere else in the app. If you check the App Header, the Add button, which later changes to Close, is the button component, and in this section, we will be creating the button.

In the Button.js file you created in the components folder paste the code below.

const Button = ({ color, text, onClick }) => {
return <button onClick={onClick} style={{ backgroundColor: color }} className="btn">{text}</button>
}
export default Button

In this component, three props (color, text, onClick) were passed into it from the Header Component, which we will create in the next section.

Header Component

The Header Component consists of the App Name, which is "Task Manager App" and the imported Button Component. In this component, we also have two props which are (showForm, changeTextColor), and they are both used to show the form and change the text and color of the button, respectively.

import React from 'react';
import Button from './Button';
import "../index.css";
const Header = ({ showForm, changeTextAndColor }) => {
return (
<header className="header">
<h2 className="app-header">Task Manager App</h2>
<Button onClick={showForm} color={changeTextAndColor ? 'red' : 'green'} text= {changeTextAndColor ? 'Close' : 'Add'} />
</header>
)
}
export default Header;

The Header Component will also be styled with the CSS code below:

.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.app-header {
font-family: "Josefin Sans", sans-serif;
}

Task Component

This Component will define the structure of our added tasks. We will build this component according to the user interface in our complete App look.

In this component, we will be using two icons from the React Icons library for the edit and delete features. We will also pass three props which are (task, onDelete, onEdit). The purpose of these props is to show the added, deleted, and edited tasks.

import { FaPencilAlt, FaTimes } from 'react-icons/fa';
import "../index.css"
const Task = ({ task, onDelete, onEdit }) => {
return (
<div>
<div className="task">
<div>
<p className="taskName">
<span className="textBold">Task Name:</span> {task.text}
</p>
<p className="taskDate"><span className="textBold">Date of Completion:</span> {task.day}
</p>
</div>
<div>
<p><FaTimes onClick={() => onDelete(task.id)} className="delIcon" /></p>
<p><FaPencilAlt onClick={() => onEdit(task.id)} className="editIcon" /></p>
</div>
</div>
</div>
)
}
export default Task;

This Component will also come out nice by styling it with the CSS below:

.task {
background: #f4f4f4;
margin: 5px;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
display: flex;
justify-content: space-between;
}
.textBold {
font-weight: bold;
}
.delIcon {
color: red;
cursor: pointer;
}
.editIcon {
color: blue;
cursor: pointer;
}

Then let's move to create our last Component!

Tasks Component

This Component is very short and precise. It receives the tasks, onDelete and onEdit props then maps through the tasks, and passing each task down along with the edit and delete functions to the Task Component we created above.

import Task from './Task';
import "../index.css"
const Tasks = ({ tasks, onDelete, onEdit }) => {
return (
<>
{
tasks.map((task) => (
<Task key={task.id} task={task} onDelete={onDelete} onEdit={onEdit} />
))
}
</>
)
}
export default Tasks;

At this point, we are done with building all of the Components of our Task Manager App.

Now we can now move on to adding the CRUD operations and some other minor features to our app.

App Features

The following is the main section of our tutorial. Here we will be adding the CRUD operations.

App.js Set-up

Before we start writing the functions that perform all these features, let's prepare our App.js file by importing all the components and libraries that are needed, and set up the display of our app.

App.js Imports

Navigate to src/App.js. In this file, we need three components that we created in the previous section, the useState and useEffect hooks, as well as the uuid and sweetalert2 libraries.

// Components
import Header from './components/Header';
import Tasks from './components/Tasks';
import AddTask from './components/AddTask';
// Hooks
import { useState, useEffect } from 'react';
// Packages
import { v4 as uuidv4 } from 'uuid';
import Swal from "sweetalert2";

note: useEffect

It allows us to implement all of the lifecycle methods from within a single function API. It is used majorly when a component renders just like "componentDidMount" in a Class-based component.

useEffect(() => {
alert("Welcome to our App");
}, []);

After the imports, let's render all the components in our App component, but before we do that, we need to make use of the useState hook to reveal the AddTask Component, and check that we have more than 0 tasks.

const [tasks, setTasks] = useState([]); // Task State
const [showAddTask, setShowAddTask] = useState(false); // To reveal add task form

An added task will be pushed into the tasks state array.

The showAddTask state is false initially, which means the AddTask form will not show unless it is true, and to toggle it to true, we use the button in the Header Component.

We also pass the function addTask through an onSave prop in the AddTask component. We'll write that function next.

function App() {
// All States
const [tasks, setTasks] = useState([]); // Task State
const [showAddTask, setShowAddTask] = useState(false); // To reveal add task form
return (
<>
<div className="container">
{/* App Header */}
<Header showForm={() => setShowAddTask(!showAddTask)} changeTextAndColor={showAddTask} />
{/* Revealing the Add Task Form */}
{showAddTask && <AddTask onSave={addTask} />}
{/* Displaying Tasks */}
{
tasks.length > 0 ?
(<Tasks tasks={tasks} />) :
('No Task Found!')
}
</div>
</>
)
}
export default App;

Our App.js is set. Without further ado, let's dive into the features in the order below.

  • Creating Tasks.
  • Updating Tasks.
  • Deleting Tasks.
  • Adding the Counter Feature.

Creating Tasks

We created the form component and validated it to make sure the data from the form is correct. So, we just need to write a function that gets the data using the task argument, and then set the tasks state to the form data so that the tasks state can be mapped through and to display the tasks on our page.

  • Gets the form data through the task argument
  • Creates a unique ID using the uuid library
  • Creates a newTask object with the id and spread task arg
  • Uses the setTasks functions to add the newTask to the end of the task state array
  • Uses the sweetalert2 library to fire an alert when a task is created successfully
// Add Task
const addTask = (task) => {
const id = uuidv4();
const newTask = { id, ...task }
setTasks([...tasks, newTask]);
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully added a new task!'
})
}

Deleting Tasks

Deleting is one of the core features of the CRUD operation, and in this section, we will build this feature.

We need to delete items by id. So, if I have the id of the item I want to delete, I can filter the list of all items by id and return a list of items without the item with a matching id.

Next thingis to create a function that carries out our delete feature, The name of our function is deleteTask. The id we are passing as argument in the function below is gotten from the Task Component we created above, and below is a code showing it:

<p><FaTimes onClick={() => onDelete(task.id)} className="delIcon" /></p>

So clicking the "FaTimes" icon fires the deleteTask function that was passed down through the onDelete props which was passed down from the App component down to Tasks then to Task.

If a task is successfully deleted, it throws an alert saying, "You have successfully deleted a task!"

// Delete Task
const deleteTask = (id) => {
const deleteTask = tasks.filter((task) => task.id !== id);
setTasks(deleteTask);
Swal.fire({
icon: 'success',
title: 'Oops...',
text: 'You have successfully deleted a task!'
})
}

Updating Tasks

Like in our delete feature, we will use id to find our unique item and update it. We will also use a window prompt to receive new data from the user so that it can replace the previous data.

In the Task Component, when we click on the edit icon, which was gotten from react icons, it calls the editTask function in the App.js file. So the editTask function was passed into this component as the onEdit prop.

<p><FaPencilAlt onClick={() => onEdit(task.id)} className="editIcon" /></p>

Below is the function that takes in the task id and prompts a user to fill new changes to the task and, before it can get updated, we need to map through our tasks id and get that particular one so that we can set that id's data to the new data we are filling. After this, the task gets updated.

Bellow is the editTask function definition. In the function we assign the text and day variables with values from prompts.

We then map through the tasks state array. If the task's id matches the id argument, we return an object with the new data. Otherwise, we just return the task.

If a task is successfully updated, it throws an alert saying "You have successfully edited an existing task!"

// Edit Task
const editTask = (id) => {
const text = prompt("Task Name");
const day = prompt("Day and Time");
const myData = tasks.map(x => {
if (x.id === id) {
return {
...x,
text: text,
day: day,
id: uuidv4()
}
}
return x;
})
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully edited an existing task!'
})
}

Ready to pick up the pace?

Enter your email and receive regular updates on our latest articles and courses

What do you want to take to the next level?

Adding the Counter Feature.

This is the easiest feature because it is just about getting the length of the tasks state array, so below is a code snippet that shows how to get a count of all tasks added.

{/* Task Counter */}
<h3>Number of Tasks: {tasks.length}</h3>

Adding the Loading Spinner

In real-world applications where you will use an API, you need to add a spinner/loader that is displayed before your data from the backend is ready to be shown to the users.

In this case, we are not using an API, but for practice, let us add a spinner that loads before our main page renders.

There are a lot of React spinners or loaders we can use but let's use a Bootstrap spinner to keep this app very simple but awesome. This is the link to the Bootstrap Docs where you can find different Bootstrap spinners, and you are free to check out any of them. You can also check out React Spinners like those from the react-spinners library.

We would be adding our spinner to the App.js file because this is where all the components in our application are rendered.

There are just three things to do:

  • Create a loading state using React's useState hook
  • Set our loading to false after page renders using useEffect hook
  • Put a condition in our page render to check if loading is true or false.

Setup the loading state. The initial state should be true.

const [loading, setloading] = useState(true); // Pre-loader before page renders

Setting our loading state to false with useEffect after three and a half seconds. This behavior simulates waiting for an API call to return data to us.

useEffect(() => {
setTimeout(() => {
setloading(false);
}, 3500);
}, [])

Now, we can use a ternary that checks if the loading state is true, if it is our spinner should show and our page won't render yet. If it is false our page will render.

return (
<>
{
loading ?
<div className="spinnerContainer">
<div className="spinner-grow text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-secondary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-success" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-danger" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-warning" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div> :
<div className="container">
{/* App Header that has open and App Name */}
<Header showForm={() => setShowAddTask(!showAddTask)} changeTextAndColor={showAddTask} />
{/* Revealing of Add Task Form */}
{showAddTask && <AddTask onSave={addTask} />}
{/* Task Counter */}
<h3>Number of Tasks: {tasks.length}</h3>
{/* Displaying of Tasks */}
{
tasks.length > 0 ?
(<Tasks tasks={tasks} onDelete={deleteTask} onEdit={editTask} />) :
('No Task Found!')
}
</div>
}
</>
)

Let’s add little styling to our spinner. Still in the index.css file, we would style the spinnerContainer as shown below:

.spinnerContainer {
margin: 20% auto;
max-width: 15%;
}

Using Local Storage

The localStorage object stores data without an expiration date. The data will not be deleted when the browser is closed. In this App, we will be using LocalStorage to store our tasks data so the user has access to them even after leaving the page in any way.

In production you would use a database of some kind but we are using LocalStorage as practice.

We'll be using the following localStorage methods:

Fetching our App Data from Local Storage

Before this section, we were getting our app data from the tasks state, but all our data will be empty once we reload, so in order to persist our data let's set our state with the data in localStorage.

So once the page loads using useEffect, it loads the app data from localStorage.

// Fetching from Local Storage
const getTasks = JSON.parse(localStorage.getItem("taskAdded"));
useEffect(() => {
if (getTasks == null) {
setTasks([])
} else {
setTasks(getTasks);
}
}, [])

Saving Added Tasks to LocalStorage

At the end of the addTask function we wrote earlier, we would need to save the data the user is adding by using the above syntax of LocalStorage. So it will look like the code below. Our key is taskAdded and value is newTask.

localStorage.setItem("taskAdded", JSON.stringify([...tasks, newTask]));

Our overall addTask function after implementing LocalStorage should look like this:

const addTask = (task) => {
const id = uuidv4();
const newTask = { id, ...task }
setTasks([...tasks, newTask]);
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully added a new task!'
})
localStorage.setItem("taskAdded", JSON.stringify([...tasks, newTask]));
}

Updating LocalStorage for Deleted Tasks

We would need to also set the result after an item is deleted to localStorage so that it can be current with our latest changes.

So using the same key but now another value, let's set it to the result of the deleteTask function.

localStorage.setItem("taskAdded", JSON.stringify(deleteTask));

Our deleteTask function should look like this:

// Delete Task
const deleteTask = (id) => {
const deleteTask = tasks.filter((task) => task.id !== id);
setTasks(deleteTask);
Swal.fire({
icon: 'success',
title: 'Oops...',
text: 'You have successfully deleted a task!'
})
localStorage.setItem("taskAdded", JSON.stringify(deleteTask));
}

Updating LocalStorage for Edited Tasks

We also need to set the result after an item is edited to localStorage so that it can be current with our latest changes.

So we get our data from localStorage, map through it, then edit it the task with a matching id property. Using the same key but now another value, lets set the new data to localStorage with our editTask function. Our app also needs to be reloaded so that our state can carry the latest changes.

localStorage.setItem("taskAdded", JSON.stringify(myData));
window.location.reload();

Our editTask function should look like this:

// Edit Task
const editTask = (id) => {
const text = prompt("Task Name");
const day = prompt("Day and Time");
let data = JSON.parse(localStorage.getItem('taskAdded'));
const myData = data.map(x => {
if (x.id === id) {
return {
...x,
text: text,
day: day,
id: uuidv4()
}
}
return x;
})
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully edited an existing task!'
})
localStorage.setItem("taskAdded", JSON.stringify(myData));
window.location.reload();
}

At this point, our tasks are being fetched from localStorage. If you edit or delete a task, it will also be updated in localStorage.

Our final App.js file should look like this:

// Importing Components
import Header from './components/Header';
import Tasks from './components/Tasks';
import AddTask from './components/AddTask';
// Importing React Hooks
import { useState, useEffect } from 'react';
// Importing Packages
import { v4 as uuidv4 } from 'uuid';
import Swal from "sweetalert2";
function App() {
// All States
const [loading, setloading] = useState(true); // Pre-loader before page renders
const [tasks, setTasks] = useState([]); // Task State
const [showAddTask, setShowAddTask] = useState(false); // To reveal add task form
// Pre-loader
useEffect(() => {
setTimeout(() => {
setloading(false);
}, 3500);
}, [])
// Fetching from Local Storage
const getTasks = JSON.parse(localStorage.getItem("taskAdded"));
useEffect(() => {
if (getTasks == null) {
setTasks([])
} else {
setTasks(getTasks);
}
}, [])
// Add Task
const addTask = (task) => {
const id = uuidv4();
const newTask = { id, ...task }
setTasks([...tasks, newTask]);
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully added a new task!'
})
localStorage.setItem("taskAdded", JSON.stringify([...tasks, newTask]));
}
// Delete Task
const deleteTask = (id) => {
const deleteTask = tasks.filter((task) => task.id !== id);
setTasks(deleteTask);
Swal.fire({
icon: 'success',
title: 'Oops...',
text: 'You have successfully deleted a task!'
})
localStorage.setItem("taskAdded", JSON.stringify(deleteTask));
}
// Edit Task
const editTask = (id) => {
const text = prompt("Task Name");
const day = prompt("Day and Time");
let data = JSON.parse(localStorage.getItem('taskAdded'));
const myData = data.map(x => {
if (x.id === id) {
return {
...x,
text: text,
day: day,
id: uuidv4()
}
}
return x;
})
Swal.fire({
icon: 'success',
title: 'Yay...',
text: 'You have successfully edited an existing task!'
})
localStorage.setItem("taskAdded", JSON.stringify(myData));
window.location.reload();
}
return (
<>
{
loading ?
<div className="spinnerContainer">
<div className="spinner-grow text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-secondary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-success" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-danger" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<div className="spinner-grow text-warning" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div> :
<div className="container">
{/* App Header that has open and App Name */}
<Header showForm={() => setShowAddTask(!showAddTask)} changeTextAndColor={showAddTask} />
{/* Revealing of Add Task Form */}
{showAddTask && <AddTask onSave={addTask} />}
{/* Task Counter */}
<h3>Number of Tasks: {tasks.length}</h3>
{/* Displaying of Tasks */}
{
tasks.length > 0 ?
(<Tasks tasks={tasks} onDelete={deleteTask} onEdit={editTask} />) :
('No Task Found!')
}
</div>
}
</>
)
}
export default App;

Our Entire App Styles

In our App, we use a general styling like our font-family (Josefin Sans), body styling, and a general container for our app to make it look well across desktop to mobile screens. All the styles we used above will also be there.

So open your index.css file to paste the styles below there:

@import url("https://fonts.googleapis.com/css2?family=Josefin+Sans&display=swap");
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Josefin Sans", sans-serif;
background-image: url("./images/bg.jpg");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-attachment: fixed;
height: 600px;
}
.spinnerContainer {
margin: 20% auto;
max-width: 15%;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.app-header {
font-family: "Josefin Sans", sans-serif;
}
.container {
max-width: 600px;
margin: 30px auto;
overflow: auto;
min-height: 300px;
padding: 30px;
border-radius: 5px;
background-color: #ccc;
}
.btn {
display: inline-block;
background: #000;
color: #fff;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
font-size: 15px;
font-family: inherit;
}
.btn:focus {
outline: none;
color: white;
}
.btn-block {
display: block;
width: 100%;
}
.task {
background: #f4f4f4;
margin: 5px auto;
max-width: 100%;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
overflow: scroll;
display: flex;
justify-content: space-between;
}
.task::-webkit-scrollbar {
display: none;
}
.textBold {
font-weight: bold;
}
.delIcon {
color: red;
cursor: pointer;
}
.editIcon {
color: blue;
cursor: pointer;
}
.add-form {
margin-bottom: 40px;
}
.form-control {
margin: 20px 0;
}
.form-control label {
display: block;
}
.form-control input {
width: 100%;
height: 40px;
margin: 5px;
padding: 3px 7px;
font-size: 17px;
}
.form-control input:focus {
outline: 0;
}
@media (max-width: 768px) {
.spinnerContainer {
margin: 250px auto 0 auto;
max-width: 30%;
}
}
@media (max-width: 425px) {
.container {
max-width: 90%;
}
.spinnerContainer {
margin: 250px auto 0 auto;
max-width: 40%;
}
}
@media (max-width: 375px) {
.container {
max-width: 95%;
}
.app-header {
font-size: 20px;
}
.spinnerContainer {
margin: 250px auto 0 auto;
max-width: 45%;
}
}
@media (max-width: 320px) {
.container {
max-width: 95%;
}
.app-header {
font-size: 18px;
}
.spinnerContainer {
margin: 250px auto 0 auto;
max-width: 50%;
}
}

Resources

Read up on some of the concepts covered in this tutorial in the links below.

Conclusion

In this tutorial, we learned how to build a CRUD App with React and Local Storage, how to set up React with create-react-app, React Hooks, React States, and Props, and I believe that with this article, every reader should be able to build a very nice CRUD App.

Here is a link to the project repo for more guides and the live project.

If you have any questions, you can always send me a message on Twitter, and I will respond.

Ready to pick up the pace?

Enter your email and receive regular updates on our latest articles and courses

What do you want to take to the next level?