Hi there, are you a developer that is just new in React and you are looking to build your first project or do you want to improve your skills as a React developer? Then this project will be helpful.
So, imagine you want an app that makes you create tasks that you want to achieve that day, update if you have changed your mind about it, and also delete if you no longer want the task.
In this article, you will be looking at building a task manager app with React and Typescript.
Requirements
You must have Nodejs installed on your machine
Basic knowledge of React is required
You must also have a basic knowledge of Typescript
Getting started with Vite
Vite is a modern build tool that is designed for fast development. It provides a smooth and efficient development experience, especially for projects using modern JavaScript (ES6+), TypeScript(which we will be using in our project), and other languages that compile JavaScript.
Follow these steps to install Vite
:
Make sure you have Node.js installed on your system. If you don't have it installed, you can download it from the official Node.js website here and follow the installation instructions.
- Open your terminal or command prompt.
Install Vite globally on your system using npm by typing this in your terminal:
npm install -g create-vite
- Create a new React app using Vite:
Replace task-manager-app
with the desired name of your application. The --template react flag tells Vite to use the React template.
create-vite task-manager-app --template react
- Change the directory to the newly created app:
cd task-manager-app
- Install node-modules and then the app's dependencies:
npm install
npm i react-icons
- Start the development server by running this command:
npm run dev
Vite will launch the development server, and your React app will be available at http://127.0.0.1:5173 by default.
Setting up the file structure
You need to clean up and set up the file structure for the app before creating any necessary components. React installation normally comes with some default files that won't be needed moving forward, so removing them and starting on a clean slate is best. So, App.js
and App.css
should be deleted from the src folder. Lastly, create a App.tsx
fiile and copy this:
function App() {
return (
<>
</>
)
}
export default App
After cleaning up, arrange and set up the folder structure you will use in the app.
So in the src
directory, you are left with your App.tsx
and main.tsx
file.
Styling the App
it is important to style your App to make it look more appealing and attractive. Copy this block of code for styling:
/*
===============
Variables
===============
*/
:root {
/* dark shades of primary color*/
--clr-primary-1: hsl(205, 86%, 17%);
--clr-primary-2: hsl(205, 77%, 27%);
--clr-primary-3: hsl(205, 72%, 37%);
--clr-primary-4: hsl(205, 63%, 48%);
/* primary/main color */
--clr-primary-5: hsl(205, 78%, 60%);
/* lighter shades of primary color */
--clr-primary-6: hsl(205, 89%, 70%);
--clr-primary-7: hsl(205, 90%, 76%);
--clr-primary-8: hsl(205, 86%, 81%);
--clr-primary-9: hsl(205, 90%, 88%);
--clr-primary-10: hsl(205, 100%, 96%);
/* darkest grey - used for headings */
--clr-grey-1: hsl(209, 61%, 16%);
--clr-grey-2: hsl(211, 39%, 23%);
--clr-grey-3: hsl(209, 34%, 30%);
--clr-grey-4: hsl(209, 28%, 39%);
/* grey used for paragraphs */
--clr-grey-5: hsl(210, 22%, 49%);
--clr-grey-6: hsl(209, 23%, 60%);
--clr-grey-7: hsl(211, 27%, 70%);
--clr-grey-8: hsl(210, 31%, 80%);
--clr-grey-9: hsl(212, 33%, 89%);
--clr-grey-10: hsl(210, 36%, 96%);
--clr-white: #fff;
--clr-red-dark: hsl(360, 67%, 44%);
--clr-red-light: hsl(360, 71%, 66%);
--clr-green-dark: hsl(125, 67%, 44%);
--clr-green-light: hsl(125, 71%, 66%);
--clr-black: #222;
--transition: all 0.3s linear;
--spacing: 0.1rem;
--radius: 0.25rem;
--light-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
--dark-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
--max-width: 1170px;
--fixed-width: 620px;
}
/*
===============
Global Styles
===============
*/
*,
::after,
::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: var(--clr-grey-10);
color: var(--clr-grey-1);
line-height: 1.5;
font-size: 0.875rem;
}
ul {
list-style-type: none;
}
a {
text-decoration: none;
}
h1,
h2,
h3,
h4 {
letter-spacing: var(--spacing);
text-transform: capitalize;
line-height: 1.25;
margin-bottom: 0.75rem;
}
h1 {
font-size: 3rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.25rem;
}
h4 {
font-size: 0.875rem;
}
p {
margin-bottom: 1.25rem;
color: var(--clr-grey-5);
}
@media screen and (min-width: 800px) {
h1 {
font-size: 4rem;
}
h2 {
font-size: 2.5rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1rem;
}
body {
font-size: 1rem;
}
h1,
h2,
h3,
h4 {
line-height: 1;
}
}
.btn {
text-transform: uppercase;
background: transparent;
color: var(--clr-black);
padding: 0.375rem 0.75rem;
letter-spacing: var(--spacing);
display: inline-block;
transition: var(--transition);
font-size: 0.875rem;
border: 2px solid var(--clr-black);
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
border-radius: var(--radius);
}
.btn:hover {
color: var(--clr-white);
background: var(--clr-black);
}
/* section */
.section {
padding: 5rem 0;
}
.section-center {
width: 90vw;
margin: 0 auto;
max-width: 35rem;
margin-top: 8rem;
}
@media screen and (min-width: 992px) {
.section-center {
width: 95vw;
}
}
main {
min-height: 100vh;
display: grid;
place-items: center;
}
/*
===============
Grocery List
===============
*/
.section-center {
background: var(--clr-white);
border-radius: var(--radius);
box-shadow: var(--light-shadow);
transition: var(--transition);
padding: 2rem;
}
.section-center:hover {
box-shadow: var(--dark-shadow);
}
.alert {
margin-bottom: 1rem;
height: 1.25rem;
display: grid;
align-items: center;
text-align: center;
font-size: 0.7rem;
border-radius: 0.25rem;
letter-spacing: var(--spacing);
text-transform: capitalize;
}
.alert-danger {
color: #721c24;
background: #f8d7da;
}
.alert-success {
color: #155724;
background: #d4edda;
}
.grocery-form h3 {
color: var(--clr-primary-1);
margin-bottom: 1.5rem;
text-align: center;
}
.form-control {
display: flex;
justify-content: center;
}
.grocery {
padding: 0.25rem;
padding-left: 1rem;
background: var(--clr-grey-10);
border-top-left-radius: var(--radius);
border-bottom-left-radius: var(--radius);
border-color: transparent;
font-size: 1rem;
flex: 1 0 auto;
color: var(--clr-grey-5);
}
.grocery::placeholder {
font-family: var(--ff-secondary);
color: var(--clr-grey-5);
}
.submit-btn {
background: var(--clr-primary-8);
border-color: transparent;
flex: 0 0 5rem;
display: grid;
align-items: center;
padding: 0.25rem;
text-transform: capitalize;
letter-spacing: 2px;
border-top-right-radius: var(--radius);
border-bottom-right-radius: var(--radius);
cursor: pointer;
content: var(--clr-primary-5);
transition: var(--transition);
font-size: 0.85rem;
}
.submit-btn:hover {
background: var(--clr-primary-5);
color: var(--clr-white);
}
.grocery-container {
margin-top: 2rem;
}
.grocery-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
transition: var(--transition);
padding: 0.25rem 1rem;
border-radius: var(--radius);
text-transform: capitalize;
}
.grocery-item:hover {
color: var(--clr-grey-5);
background: var(--clr-grey-10);
}
.grocery-item:hover .title {
color: var(--clr-grey-5);
}
.title {
margin-bottom: 0;
color: var(--clr-grey-1);
letter-spacing: 2px;
transition: var(--transition);
}
.edit-btn,
.delete-btn {
background: transparent;
border-color: transparent;
cursor: pointer;
font-size: 0.7rem;
margin: 0 0.15rem;
transition: var(--transition);
}
.edit-btn {
color: var(--clr-green-light);
}
.edit-btn:hover {
color: var(--clr-green-dark);
}
.delete-btn {
color: var(--clr-red-light);
}
.delete-btn:hover {
color: var(--clr-red-dark);
}
.clear-btn {
text-transform: capitalize;
width: 10rem;
height: 1.5rem;
display: grid;
align-items: center;
background: transparent;
border-color: transparent;
color: var(--clr-red-light);
margin: 0 auto;
font-size: 0.85rem;
letter-spacing: var(--spacing);
cursor: pointer;
transition: var(--transition);
margin-top: 1.25rem;
}
.clear-btn:hover {
color: var(--clr-red-dark);
}
Creating necessary Components
Here are the components you will be creating:
Task: This will be the component that renders the tasks on the app
Toast: This will be the component that displays messages when a user create, updates and delete tasks on the app.
Task Component
For the Task component, create a file named task.tsx
in your src
directory and copy this block of code:
import React from 'react'
const Task = () => {
return (
<div>Task</div>
)
}
export default Task
Toast Component
Also, create a file named toast.tsx
in your src
directory and copy this block of code:
import React from 'react'
const Toast = () => {
return (
<div>Toast</div>
)
}
export default Toast
Defining types
In Typescript, it's necessary to define types because it helps you as a developer to easily refactor your code and detect your errors during the development process.
Create a folder named types in your src
directory and then also create a file named index.ts
inside your types folder where you will have your defined types and copy this:
export interface toastType {
show: boolean,
msg: string,
type: string
}
export interface listType {
id: number,
title: string
}
Implementing CRUD functionalities
In this section, you will be implementing CRUD functionalities inside the component which will enable you to create, update, and delete tasks.
Implementation for the App component
Follow these steps for the implementation:
- Firstly, Make all the necessary imports:
import { useState } from 'react'
import './App.css'
import List from './List'
import Toast from './Toast'
//import your needed types
import {toastType, listType} from "./types/index"
- Secondly, handle states using the
useState
hook:
const[inputValue, setInputValue] = useState<string>("")
const[list, setList] = useState<listType[]>([])
const [isEditing, setIsEditing] = useState<boolean>(false)
const [editId, setEditId] = useState<null | number>(null)
const[toast, setToast] = useState<toastType>({show: false, msg: "", type: ""})
- Create the
handleSubmit
function that handles the creation of a task when there is inputValue and also changes the toast state based on conditions.
const handleSubmit = ( e: React.FormEvent ) => {
e.preventDefault()
if(!inputValue){
setToast({show: true, msg: "create a task", type: "danger"})
}
else if (inputValue && isEditing) {
console.log(editId)
const updatedItems = list.map((item) => item.id == editId ? {...item, title: inputValue}: item)
setList(updatedItems)
setIsEditing(false)
setInputValue("")
}
else {
setList([...list, {id: Math.random(), title: inputValue}])
setInputValue("")
}
}
- Then Create the
editItem
function that is responsible for editing a specific item in the task whose id is equal to the id passed as a parameter to the function.
const editItem = (id: number) => {
const updatedItem = list.find((item) => item.id === id)
setIsEditing(true)
setEditId(id)
setInputValue(updatedItem?.title as string)
}
- Next, Create the
deleteItem
function that is responsible for deleting a specific item in the task whose id is equal to the id passed as a parameter to the function
const deleteItem = (id: number) => {
setToast({show: true, msg: "successfully deleted", type: 'danger'})
const updatedItems = list.filter((item) => item.id !== id)
setList(updatedItems)
}
- Lastly, Create the
clearItem
function is responsible for clearing the task completely.
const clearItem = () => {
if(list.length < 1) {
setToast({show: true, msg: "there are no items", type: "danger"})
}
else {
setList([])
setToast({show: true, msg: "successfully cleared", type: "danger"})
}
}
Inside your return function, set up the layout, pass some props to the Toast and the list component, and then pass the created functions to the event handlers.
<>
<section className='section-center'>
{toast?.show ? <Toast setToast={setToast} list={list} {...toast}/> : ""}
<form onSubmit={handleSubmit}>
<h3>Task Manager</h3>
<div className='form-control'>
<input
value={inputValue}
placeholder='create your task for today'
onChange={(e: React.FormEvent) => setInputValue((e.target as HTMLInputElement).value)}
/>
<button type='submit' className='submit-btn'>
{isEditing ? "edit" : "create"}
</button>
</div>
</form>
<div className='grocery-container'>
<List editItem={editItem} deleteItem={deleteItem} list={list}/>
<button className='clear-btn' onClick={clearItem}>clear items</button>
</div>
</section>
</>
Here is how your layout should look like after following the implementation:
import { useState } from 'react'
import './App.css'
import List from './List'
import Toast from './Toast'
import {toastType, listType} from "./types/index"
function App() {
const[inputValue, setInputValue] = useState<string>("")
const[list, setList] = useState<listType[]>([])
const [isEditing, setIsEditing] = useState<boolean>(false)
const [editId, setEditId] = useState<null | number>(null)
const[toast, setToast] = useState<toastType>({show: false, msg: "", type: ""})
const handleSubmit = ( e: React.FormEvent ) => {
e.preventDefault()
if(!inputValue){
setToast({show: true, msg: "create a task", type: "danger"})
}
else if (inputValue && isEditing) {
console.log(editId)
const updatedItems = list.map((item) => item.id == editId ? {...item, title: inputValue}: item)
setList(updatedItems)
setIsEditing(false)
setInputValue("")
}
else {
setList([...list, {id: Math.random(), title: inputValue}])
setInputValue("")
}
}
const editItem = (id: number) => {
const updatedItem = list.find((item) => item.id === id)
console.log(updatedItem?.title)
setIsEditing(true)
setEditId(id)
setInputValue(updatedItem?.title as string)
}
const deleteItem = (id: number) => {
setToast({show: true, msg: "successfully deleted", type: 'danger'})
const updatedItems = list.filter((item) => item.id !== id)
setList(updatedItems)
}
const clearItem = () => {
if(list.length < 1) {
setToast({show: true, msg: "there are no items", type: "danger"})
}
else {
setList([])
setToast({show: true, msg: "successfully cleared", type: "danger"})
}
}
return (
<>
<section className='section-center'>
{toast?.show ? <Toast setToast={setToast} list={list} {...toast}/> : ""}
<form onSubmit={handleSubmit}>
<h3>Task Manager</h3>
<div className='form-control'>
<input
value={inputValue}
placeholder='create your task for today'
onChange={(e: React.FormEvent) => setInputValue((e.target as HTMLInputElement).value)}
/>
<button type='submit' className='submit-btn'>
{isEditing ? "edit" : "create"}
</button>
</div>
</form>
<div className='grocery-container'>
<List editItem={editItem} deleteItem={deleteItem} list={list}/>
<button className='clear-btn' onClick={clearItem}>clear items</button>
</div>
</section>
</>
)
}
export default App
Implementation for the Task Component
Here are the steps for the implementation:
- Define the type of the props passed using the interface since it is typescript:
interface TaskProps {
list:listType[],
deleteItem: (id: number) => void,
editItem: (id:number) => void
}
- Make sure you destructure the props inside the Task function, pass them the TaskProps interface just to avoid type errors, map through the task array and render each item:
const Task = ({list, deleteItem, editItem}: TaskProps) => {
return (
<div>
{task.map((item) => {
const {title, id} = item
return (
<article className='grocery-item' key={id}>
<p className='title'>{title}</p>
<div className='btn-container'>
<button
type='button'
className='edit-btn'
onClick={() => editItem(id)}
>
<FaEdit />
</button>
<button
type='button'
className='delete-btn'
onClick={() => deleteItem(id)}
>
<FaTrash />
</button>
</div>
</article>
Here is what your layout should look like after the implementation:
import React from 'react'
import {FaTrash, FaEdit} from "react-icons/fa"
import { listType } from './types/index'
interface TaskProps {
list:listType[],
deleteItem: (id: number) => void,
editItem: (id:number) => void
}
const Task = ({list, deleteItem, editItem}: TaskProps) => {
return (
<div>
{list.map((item) => {
const {title, id} = item
return (
<article className='grocery-item' key={id}>
<p className='title'>{title}</p>
<div className='btn-container'>
<button
type='button'
className='edit-btn'
onClick={() => editItem(id)}
>
<FaEdit />
</button>
<button
type='button'
className='delete-btn'
onClick={() => deleteItem(id)}
>
<FaTrash />
</button>
</div>
</article>
)}
)}
</div>
)
}
export default Task
Implementation for the Toast Component
Follow the steps for the implementation:
For the Toast Component, make all the necessary imports:
import React, {useEffect} from 'react'
import { toastType, listType } from './types/index'
- Create an interface that defines the props type that is passed to the Toast component:
interface ToastProps {
show: boolean,
type: string,
msg: string,
setToast: React.Dispatch<React.SetStateAction<toastType>>,
list: listType[]
}
With the useEffect hook, you get to handle the toast state by allowing the toast message to disappear after three seconds.
useEffect(() => {
let timeoutId: any
if(msg) {
timeoutId = setTimeout(() => {
setToast({show: false, msg: "", type: ""})
},3000)
return () => {
clearTimeout(timeoutId)
}
}
},[msg, list])
After following the implementation, this is what your layout should look like:
import React, {useEffect} from 'react'
import { toastType, listType } from './types/index'
interface ToastProps {
show: boolean,
type: string,
msg: string,
setToast: React.Dispatch<React.SetStateAction<toastType>>,
list: listType[]
}
const Toast = ({show, type, msg, setToast, list}: ToastProps)=> {
useEffect(() => {
let timeoutId: any
if(msg) {
timeoutId = setTimeout(() => {
setToast({show: false, msg: "", type: ""})
},3000)
return () => {
clearTimeout(timeoutId)
}
}
},[msg, list])
return (
<div>
<p className={`alert alert-${type}`}>{msg}</p>
</div>
)
}
export default Toast
Here is what your project should look like on http://127.0.0.1:5173
Conclusion
If you have any questions regarding this project, please don't hesitate to ask questions in the comment section.
Toodles and good luck...