Getting started with Full Stack Web Development - MERN Stack

Part 3 - React and Redux Creating a functional TODO APP

In the last part of this tutorial, we finished implementing all of our backend required to start implementing the frontend. In this part of the tutorial, we will start with our frontend, we start by running npx create-react-app frontend --template redux in our root folder. This will help us set up our frontend folder using redux. tree9.PNG

NOTE: This tutorial will not cover the styling aspect of this app, you can get the stylesheet used from here, add the style folder to src folder

Also, in our frontend folder especially the src folder, we want to clean up some stuff redux provided for us

  • Delete everything inside the features folder in
  • Delete App.css, index.css and logo.svg
  • In store.js located in the app folder, delete the counterReducer and the corresponding import.
  • We will also install some packages that will be used by our frontend, in the frontend folder run npm install axios react-router-dom react-spinners react-toastify react-tsparticles sass tsparticles

Interacting with our backend from the frontend using react-redux and redux toolkit

We install a package in our root folder as a dev dependency since we are only using it in development, that allows us to run both the frontend and backend simultaneously, run npm install -D concurrently. In package.json located in our root folder, in the scripts object, add

"client": "npm start --prefix frontend",
"dev": "concurrently \"npm run start\" \"npm run client\""

In the root folder, run npm run dev to start both the frontend and backend server terminal1.PNG

You should see something similar to the above, to show that concurrently has started running.

  • In features folder, create a new folder auth, this folder will handle everything related to user sign-in, sign up and logout.
  • Create a new file authService.js, this file will contain the main interaction with our backend server, before will get started with this file, in package.json located in frontend folder, add this which will allow all requests from the frontend use the proxy URL as the root URL(the server on which our backend is running)
    "proxy": "http://localhost:5000"
    

In authService.js,

import axios from 'axios';

const API_URL = '/api/user/';
// because of the proxy added in package.json, it gets interpreted to http://localhost:5000/api/user


const registerUser = async (userData) => {
  const response = await axios.post(API_URL, userData)

  if (response.data) {
    localStorage.setItem('user', JSON.stringify(response.data))
  }

  return response.data
}

const loginUser = async (userData) => {
  const response = await axios.post(API_URL + 'login', userData)

  if (response.data) {
    localStorage.setItem('user', JSON.stringify(response.data))
  }

  return response.data
}

const logout = () => {
  localStorage.removeItem('user')
}
  • Create a new file authSice.js in auth folder, this file will handle state changes using redux
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import authService from "./authService";

const user = JSON.parse(localStorage.getItem('user'))
// We get the stored user from the localStorage

const initialState = {
  user: user ? user : null,
  isLoading: false,
  isSuccess: false,
  isError: false,
  message: ''
}
// We define the initial state of our app for `auth`

// Register user
export const register = createAsyncThunk('auth/register', async (user, thunkAPI) => {
  try {
    return await authService.registerUser(user)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()
    return thunkAPI.rejectWithValue(message)
  }
})

// Login User
export const login = createAsyncThunk('auth/login',async (userData, thunkAPI) => {
  try {
    return await authService.loginUser(userData)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()

    return thunkAPI.rejectWithValue(message)
  }
})

export const logout = createAsyncThunk('auth/logout', () => {
  authService.logout()
})


export const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    reset: (state) => {
      state.isLoading = false
      state.isError = false
      state.message = ''
      state.isSuccess = false
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(register.pending, state => {
        state.isLoading = true
      })
      .addCase(register.fulfilled, (state, action) => {
        state.isLoading = false
        state.isSuccess = true
        state.user = action.payload
      })
      .addCase(register.rejected, (state, action) => {
        state.isLoading = false
        state.isError = true
        state.message = action.payload
      })
      .addCase(login.pending, (state) => {
        state.isLoading = true
      })
      .addCase(login.fulfilled, (state, action) => {
        state.isSuccess = true
        state.isLoading = false
        state.user = action.payload
      })
      .addCase(login.rejected, (state, action) => {
        state.isError = true
        state.isLoading = false
        state.message = action.payload
      })
      .addCase(logout.fulfilled, (state) => {
        state.user = null
      })
  }
})

/* The above are some redux actions on the functions defined, 
the extraReducers have three methods, `pending, fulfilled and rejected`, 
the value of the states are defined based on the three actions executed by redux
*/ 
export const { reset } = authSlice.actions
export default authSlice.reducer

We then import this authSlice.reducer as authReducer into store.js in app folder

import { configureStore } from '@reduxjs/toolkit'
import authReducer from '../features/auth/authSlice';

export default configureStore({
  reducer: {
    auth: authReducer
  }
})
  • We follow the same approach as above for actions on todo, create a new folder todos in features folder, create a new file todoService.js
import axios from 'axios'

const API_URL = '/api/todos/'


// Create Todo
const createTodo = async (newTask, token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  }

/* The config object contains the token that will be passed since this 
route is protected from our backend, the same applies to all other routes too.
*/
  const response = await axios.post(API_URL, newTask, config)

  return response.data
}


// Get all Todos
const getAllTodos = async (token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  }
  const response = await axios.get(API_URL, config)

  return response.data
}

const deleteTodo = async (id, token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  }

  const response = await axios.delete(API_URL + id, config)

  return response.data
}

const updateTodo = async (id, newData, token) => {
  const config = {
    headers: {
      Authorization: `Bearer ${token}`
    }
  }

  const response = await axios.put(API_URL + id, newData, config)

  return response.data
}

const todoService = {
  createTodo,
  getAllTodos,
  deleteTodo,
  updateTodo
}

export default todoService
  • Create a new file todoSlice.js in todos folder
import { createAsyncThunk, createSlice, createSelector } from "@reduxjs/toolkit";
import todoService from "./todoService";

const initialState = {
  todos: [],
  isFiltering: false,
  isLoading: false,
  isSuccess: false,
  isError: false,
  message: ''
}

export const getTodos = createAsyncThunk('todo/getAll', async(_, thunkAPI) => {
  try {
    const token = thunkAPI.getState().auth.user.token
// token is extracted from the saved user in our auth state, the same applies to all other actions
    return await todoService.getAllTodos(token)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()

    return thunkAPI.rejectWithValue(message)
  }
})

export const createTodo = createAsyncThunk('todo/createNew', async(newTask, thunkAPI) => {
  try {
    const token = thunkAPI.getState().auth.user.token
    return await todoService.createTodo(newTask, token)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()

    return thunkAPI.rejectWithValue(message)
  }
}) 

export const deleteTodo = createAsyncThunk('todo/delete', async (id, thunkAPI) => {
  try {
    const token = thunkAPI.getState().auth.user.token
    return await todoService.deleteTodo(id, token)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()

    return thunkAPI.rejectWithValue(message)
  }
})

export const updateTodo = createAsyncThunk('todo/update', async ({id, newData}, thunkAPI) => {
  try {
    const token = thunkAPI.getState().auth.user.token

    return await todoService.updateTodo(id, newData, token)
  } catch (error) {
    const message = (error.response && error.response.data && error.response.data.message) || error.message || error.toString()

    return thunkAPI.rejectWithValue(message)
  }
})

export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: {
    resetTodo: (state) => {
      state.isLoading = false
      state.isSuccess = false
      state.message = ''
      state.isError = false
    }
  },
  extraReducers: builder => {
    builder
      .addCase(getTodos.pending, (state) => {
        state.isLoading = true
      })
      .addCase(getTodos.fulfilled, (state, action) => {
        state.isLoading = false
        state.isSuccess = true
        state.todos = action.payload
      })
      .addCase(getTodos.rejected, (state, action) => {
        state.isLoading= false
        state.isError = true
        state.message = action.payload
      })
      .addCase(createTodo.pending, state => {
        state.isLoading = true
      })
      .addCase(createTodo.fulfilled, (state, action) => {
        state.isLoading = false
        state.isSuccess = true
        state.todos = [...state.todos, action.payload]
      })
      .addCase(createTodo.rejected, (state, action) => {
        state.isLoading = false
        state.isError = true
        state.message = action.payload
      })
      .addCase(deleteTodo.pending, (state) => {
        state.isLoading = true
      })
      .addCase(deleteTodo.fulfilled, (state, action) => {
        state.isLoading= false
        state.isSuccess= true
        state.todos = state.todos.filter(todo => todo._id !== action.payload.id)
      })
      .addCase(deleteTodo.rejected, (state, action) => {
        state.isLoading = false
        state.isError = true
        state.message = action.payload
      })
      .addCase(updateTodo.pending, (state) => {
        state.isLoading = true
      })
      .addCase(updateTodo.fulfilled, (state, action) => {
        state.isLoading = false
        state.isSuccess = true
        state.todos = state.todos.map(todo => {
          if (todo._id === action.payload._id) {
            return action.payload
          } else {
            return todo
          }
        })
      })
      .addCase(updateTodo.rejected, (state, action) => {
        state.isLoading = false
        state.isError = true
        state.message = action.payload
      })
  }
})

/* Since filtering will be implemented for todo to get active todos, 
completed todos and number of active todos left, we will be implementing createSelector from redux
*/
const selectTodos = state => state.todos
export const getCompletedTodos = createSelector(selectTodos, todos => todos.filter(todo => todo.completed))

export const getActiveTodos = createSelector(selectTodos, todos => todos.filter(todo => !todo.completed))

export const getAllTodos = createSelector(selectTodos, todos => todos.map(todo => todo))


export const { resetTodo } = todoSlice.actions
export default todoSlice.reducer

We need to also import todoSlice.reducer as todoReducer into our store.js

import { configureStore } from '@reduxjs/toolkit'
import authReducer from '../features/auth/authSlice';
import todoReducer from '../features/todos/todoSlice';

export default configureStore({
  reducer: {
    auth: authReducer,
    todo: todoReducer
  }
})

Since all our state is defined, we can go ahead to implement this in our UI

Creating the user interface, experience and design of our frontend

DON'T GIVE UP NOW, IT GETS INTERESTING FROM HERE...

Our UI is gotten from a challenge in frontend mentor

In App.js, we define the routes of our app

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Register from './pages/Register';
import Index from './pages/Index';
import UserHomePage from './pages/UserHomePage';

import './styles/globals.scss';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css'

function App() {
  return (
    <>
    <Router>
      <Routes>
        <Route path='/signin' element={<Index />}/>
        <Route path='/register' element={<Register />} />
        <Route path='/' element={<UserHomePage />} />
      </Routes>
    </Router>
    <ToastContainer />
    </>
  );
}

export default App;
  • Create a new folder in src called pages and components, create a new file in pages, index.jsx which will point to the /signin route as defined in App.js.
import ParticleLoader from "../components/ParticleLoader";
import SignInForm from "../components/SignInForm";
import style from '../styles/pages/index.module.scss';

function Index() {
  return (
    <>
      <div className={style.main}>
        <ParticleLoader />
        <div>
          <h1>Welcome to Todo</h1>
          <SignInForm />
        </div>
      </div>
    </>
  )
}
export default Index
  • In components folder, create a new file ParticleLoader.jsx
import Particles from 'react-tsparticles';
import { loadFull } from 'tsparticles';

function ParticleLoader() {
  const particlesInit = async(main) => {

    await loadFull(main)
  }

  const particlesLoaded = (container) => {
  }
  return (
    <Particles 
        id="tsparticles"
        init={particlesInit}
        loaded={particlesLoaded}
        options={{
          fpsLimit: 60,
          interactivity: {
            events: {
              resize: true
            },
            modes: {
              push: {
                quantity: 4
              },
              attract: {
                distance: 200,
                duration: 0.4,
                factor: 5
              }
            } 
          },
          particles:{
            color: {
              value: '#ffffff'
            },
            line_linked: {
              color: '#ffffff',
              distance: 150,
              enable: true,
              opacity: 0.4,
              width: 1
            },
            move: {
              attract: {
                enable: false,
                rotateX: 500,
                rotateY: 1000
              },
              bounce: false,
              direction: 'none',
              enable: true,
              out_model: "out",
              random: false,
              speed: 1.5,
              straight: false
            },
            number: {
              density: {
                enable: true,
                value_area: 800
              },
              value: 80
            },
            opacity: {
              anim: {
                enable: false,
                opacity_min: 0.1,
                speed: 1,
                sync: false
              },
              random: false,
              value: 0.5
            },
          }
        }
      }
      />
  )
}
export default ParticleLoader

We will create a Spinner component Spinner.jsx,

import ClipLoader from 'react-spinners/ClipLoader';

const override = {
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  margin: "auto",
  borderColor: "black"
};
function Spinner() {
  return (
    <ClipLoader 
      color='#fff'
      loading={true}
      cssOverride={override}
      size={150}
    />
  )
}
export default Spinner

For our SIgnInForm component, create a new file in compoents folder SignInForm.jsx

import style from '../styles/components/form.module.scss';
import { Link } from 'react-router-dom';

import { useDispatch, useSelector } from 'react-redux';
import { reset, login } from '../features/auth/authSlice'
import { useNavigate }from 'react-router-dom'
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import Spinner from './Spinner';

function SignInForm() {
  const { user, isSuccess, isError, message, isLoading } = useSelector(state => state.auth);
// This will bring in the state from our store

  const navigate = useNavigate()
  const dispatch = useDispatch()

  const [formData, setFormData] = useState({
    email: '',
    password: ''
  })


  const onChange = (e) => {
    setFormData(prevState => (
      {
        ...prevState,
        [e.target.name]: e.target.value
      }
    ))
  }

  useEffect(() => {
    if (isError) {
      toast.error(message)
    }

    if ((user && user.name) && isSuccess) {
      navigate('/')
    }

    dispatch(reset())

  }, [isError, message, isSuccess, navigate, user, dispatch])

  const signin = (e) => {
    e.preventDefault()

    dispatch(login(formData))
 // This executes the login action defined from authSlice using the formData

  }

  if (isLoading) {
    return <Spinner />
  }
  return (
    <div className={style.formContainer}>
            <main>
              <form onSubmit={signin}>
                <h2>Sign In</h2>
                <div>
                  <label htmlFor="email">Email</label>
                  <input type="email" name="email" onChange={onChange}/>
                </div>
                <div>
                  <label htmlFor="password">Password</label>
                  <input type="password" name="password" onChange={onChange}/>
                </div>
                <div>
                <input type="submit" value="Sign In" className={style.signin}/>
                </div>
                <div className={style.register}>
                  <Link to='/register'>
                    Register
                  </Link>
                </div>
              </form>
            </main>
          </div>
  )
}
export default SignInForm

In the pages folder, create a new file Register.jsx,

import ParticleLoader from "../components/ParticleLoader"
import RegisterForm from "../components/RegisterForm";
import style from '../styles/pages/index.module.scss';

function Register() {
  return (
    <div className={style.main}>
      <ParticleLoader />
      <RegisterForm />
    </div>
  )
}
export default Register
  • Create the RegisterForm component by creating RegisterForm.jsx in the components folder
import { Link } from 'react-router-dom';
import style from '../styles/components/form.module.scss';
import {toast} from 'react-toastify';

import { useSelector, useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';

import { register, reset } from '../features/auth/authSlice';
import { useEffect } from 'react';
import Spinner from './Spinner';


function Register() {
  const dispatch = useDispatch()
  const navigate = useNavigate()

  const { user, isLoading, isError, isSuccess, message } = useSelector(state => state.auth)
// This gets the state from store.js

  useEffect(() => {
    if (isError) {
      toast.error(message)
    }

    if (user || isSuccess) {
      navigate('/')
    }

    dispatch(reset())

  }, [user, isError, isSuccess, message, navigate, dispatch])

  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    password2: ''
  })

  const { name, email, password, password2 } = formData;

  const onChange = (e) => {
    setFormData((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value
    })
  )}

  const onSubmit = (e) => {
    e.preventDefault()

    if (password !== password2) {
      toast.error('Password do not match')
    }

    else {
      const userData = {
        name,
        email,
        password
      }
      dispatch(register(userData))
//   This executes the register action defined in authSlice.js
    }
  }

  if (isLoading) {
    return <Spinner />
  }


  return (
    <div className={style.formContainer}>
    <main>
      <form onSubmit={onSubmit}>
        <h2>Register</h2>
        <div>
          <label htmlFor="name">Name</label>
          <input type="text" name="name" onChange={onChange}/>
        </div>
        <div>
          <label htmlFor="email">Email</label>
          <input type="email" name="email" onChange={onChange}/>
        </div>
        <div>
          <label htmlFor="password">Password</label>
          <input type="password" name="password" onChange={onChange}/>
        </div>
        <div>
          <label htmlFor="password">Enter Password Again</label>
          <input type="password" name="password2" onChange={onChange}/>
        </div>
        <div>
          <input type="submit" value="Register" className={style.signin}/>
        </div>
        <div className={style.register}>
          <Link to='/signin'>
            Signin
          </Link>
        </div>
      </form>
  </main>
</div>
  )
}
export default Register
  • In pages folder, create a new file userHomePage.jsx
import Background from "../components/Background"
import TodoContainer from "../components/TodoContainer"
import style from '../styles/pages/userHomePage.module.scss';

import { useSelector, useDispatch } from 'react-redux'
import { useNavigate } from 'react-router-dom';
import { useEffect, useContext } from 'react';
import { reset } from '../features/auth/authSlice';
import Spinner from "../components/Spinner";


import { getTodos , resetTodo } from '../features/todos/todoSlice';


import { toast } from 'react-toastify';

function UserHomePage() {

  const dispatch = useDispatch()
  const navigate = useNavigate()
  const { user, isError, isLoading , message} = useSelector(state => state.auth);


  useEffect(() => {
    if (!user) {
      navigate('/signin')
    }

    if (user && user.name) {
      dispatch(getTodos())
    }

    if (isError) {
      toast.error(message)
    }

    dispatch(reset())
    dispatch(resetTodo())
  }, [user, dispatch, navigate, isError, message])

  if (isLoading || !user) {
    return <Spinner />
  }
  return (
    <div className={style.body}>
      <Background />
      <TodoContainer />
    </div>
  )
}
export default UserHomePage
  • Create the Background component Background.jsx
import style from '../styles/components/background.module.scss';
import { logout } from '../features/auth/authSlice';
import { useDispatch, useSelector } from 'react-redux'

function Background() {
  const dispatch = useDispatch()
  const { user } = useSelector(state => state.auth)

  return (
      <>
        <div className={style.image}>
          <img src="/images/bg-desktop-light.jpg"  alt="" />
        </div>
        <div className={style.userDetails}>
          <h2>Welcome {user.name}</h2>
          <p onClick={() => dispatch(logout())}>Logout</p>
        </div>
      </>
  )
}
export default Background
  • Create the TodoContainer component TodoContainer.jsx
import Logo from './Logo'
import style from '../styles/components/todoContainer.module.scss';
import TodoLists from './TodoLists';

import  { useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { createTodo } from '../features/todos/todoSlice'

function TodoContainer() {
  const task = useRef(null)
  const dispatch = useDispatch()

  const handleNewTask = (e) => {
    if (e.key === 'Enter') {
      dispatch(createTodo({task: task.current.value}))
      task.current.value = ''
    }
  }

  return (
    <div className={style.container}>
      <div className={style.logo}>
        <Logo />
        <img src='/images/icon-moon.svg'  alt="theme-mode" />
      </div>
      <div className={`${style.todo} ${style.inputTodo}`}>
        <div className={style.roundCheckbox}>
          <input type="checkbox" id='check' checked={false} readOnly/>
          <label htmlFor="check"></label>
        </div>
        <input type="text" name="newTask" ref={task} onKeyDown={handleNewTask} placeholder='Create a new todo...'/>
      </div>
      <TodoLists />
    </div>
  )
}
export default TodoContainer
  • Create Logo Component Logo.jsx
import style from '../styles/components/logo.module.scss';

function Logo() {
  return (
    <h1 className={style.logo}>
      TODO
    </h1>
  )
}
export default Logo
  • Create the TodoLists components, TodoLists.jsx
import Todo from './Todo';
import TodoFooter from './TodoFooter';
import style from '../styles/components/todoList.module.scss';
import ActionList from './ActionList';

import { useSelector } from 'react-redux';
import { useState } from 'react';

import { getCompletedTodos, getActiveTodos, getAllTodos } from '../features/todos/todoSlice';

function TodoLists() {
  const [filter, setFilter] = useState('all');
  const todos  = useSelector(state => {
    if (filter === 'all') {
      return getAllTodos(state.todo)
    } else if (filter === 'active') {
      return getActiveTodos(state.todo)
    } else if (filter === 'completed') {
      return getCompletedTodos(state.todo)
    }
  })


  return (
    <>
      <div className={style.container}>
        {todos.map(todo => <Todo task={todo.task} key={todo._id} id={todo._id} completed={todo.completed}/>)}
        <TodoFooter setFilter={setFilter} filter={filter}/>
      </div>
      <ActionList setFilter={setFilter} filter={filter}/>
      </>
  )
}
export default TodoLists
  • Create TodoFooter component, TodoFooter.jsx,
import style from '../styles/components/todoFooter.module.scss';
import ActionList from './ActionList';

import { useSelector, useDispatch } from 'react-redux';
import {getActiveTodos, getCompletedTodos, deleteTodo} from '../features/todos/todoSlice';

function TodoFooter({setFilter, filter}) {
  const dispatch = useDispatch()

  const activeTodoLeft = useSelector(state => getActiveTodos(state.todo)).length
  const completedTodo = useSelector(state => getCompletedTodos(state.todo))

  const handleClearCompleted = () => {
    completedTodo.forEach(todo => dispatch(deleteTodo(todo._id)))
  }

  return (
    <div className={style.footer}>
      <p>{activeTodoLeft} items left</p>
      <div className={style.displayActionList}>
        <ActionList setFilter={setFilter} filter={filter}/>
      </div>
      <p onClick={handleClearCompleted}>Clear Completed</p>
    </div>
  )
}
export default TodoFooter
  • Create ActionList component `ActionList.jsx
import style from '../styles/components/todoFooter.module.scss';

function ActionList({filter, setFilter}) {

  return (
    <div className={`${style.action} ${style[theme]}`}>
      <p onClick={() => setFilter('all')} className={filter === 'all' ? style.active : ''}>All</p>
      <p onClick={() => setFilter('active')} className={filter === 'active' ? style.active : ''}>Active</p>
      <p onClick={() => setFilter('completed')} className={filter === 'completed' ? style.active : ''}>Completed</p>
    </div>
  )
}
export default ActionList
  • Create Todo component, Todo.jsx
import todoStyle from '../styles/components/todoContainer.module.scss';

import { deleteTodo, updateTodo } from '../features/todos/todoSlice';
import { useDispatch } from 'react-redux'

function Todo({task, id, completed}) {

  const dispatch = useDispatch()

  const handleCompleted = () => {
    dispatch(updateTodo({id, newData: {completed: !completed}}))
  }


  return (
    <div className={todoStyle.todo}>
      <div className={todoStyle.roundCheckbox}>
          <input type="checkbox" id={id} checked={completed} onChange={handleCompleted}/>
          <label htmlFor={id}></label>
      </div>
        <p className={completed ? todoStyle.checked : ''}>{task}</p>
        <div onClick={() => dispatch(deleteTodo(id))} className={todoStyle.delete}>
          <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"><path fill="#494C6B" fillRule="evenodd" d="M16.97 0l.708.707L9.546 8.84l8.132 8.132-.707.707-8.132-8.132-8.132 8.132L0 16.97l8.132-8.132L0 .707.707 0 8.84 8.132 16.971 0z"/></svg>
        </div>
    </div>
  )
}
export default Todo

Kudos for making it this far, we are finally done with our app.

The final step is deployment so that the world can see what we have built.

Click here to access part 4

All Codes can be found on here