Build a Simple Task manager app (CRUD) with React and Typescript.

·

12 min read

Build a Simple Task manager app (CRUD) with React and Typescript.

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'
  1. 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...