todo application in reactjs and material ui

Simple Todo Application With React and Material UI

Hey there everyone, hope you all are doing great. In today’s blog, we are going to create a simple Todo Application With React and Material UI. We will build this simple React.Js application and will store our information in the local storage. We will discuss every step in this blog.

Before we start building this simple application, we assume you have some basic understanding of JavaScript and the React.Js library. In this application, we will use the best and most modern approach for our React application. We will separate our code into smaller chunks so that you easily understand what it’s doing under the hood.

Application Structure of Simple Todo Application with React and Material UI

While building the application we will be keeping the best practices as a developer. We will divide our app into smaller reusable components and one main file to host our components. Our application will have three different reusable utility functionalities folders. They are our context, reducers, and hooks. These folders will have their own custom functions that will help us in creating logic for our to-do application.

Some Terms To Remember

  • Components: Components in React are basically reusable pieces of code that we can use anywhere in our application. Components can be anything like a reusable Button, Card, Form, or any stuff that we think can be reused in our application. The advantage of using the components in React is that it makes our application more organized and human-friendly.
  • Props: So if you are using JavaScript, then you have heard about the term parameter or say arguments. They can be passed to a function as variable holders of a function. Similarly, props or properties are the arguments or parameters that we pass to React Components. Props are read-only. They can’t mutate directly.
  • State: State is the component variable. They hold certain values for our components that can be changed later on during program execution. States can be mutated with the help of hooks in React.

Yay, It’s Build Time

Now let us start building our application.

The first thing we will do is install Node.Js. Also kindly check the Node.Js version as well. Now let’s create our project from scratch. But before let me mention that we will use just React and Material UI.

Installation and Setup

Now let’s create a React App from scratch

npx create-react-app todoApp
or
yarn create react-app todoApp

Once the installation is complete, open this project folder with your favorite code editor. In this case, we will use the VS Code. Also, let us quickly install the Material UI for our project. Type the following command in the project root folder in your terminal.

Folder Structure

Our final folder structure will look like this.

node_modules
public
src
|
| --> context
      | --> todos.context.js
| --> reducers
      | --> todo.reducer.js
| --> hooks
      | --> useInputState.js
      | --> useLocalStorageReducer.js
      | --> useToggleState.js
|
| --> App.js
| --> Todo.js
| --> TodoApp.js
| --> TodoList.js
| --> TodoForm.js
| --> EditTodoForm.js
| --> index.js
| --> index.css

Installing Materail UI

npm install @mui/material @emotion/react @emotion/styled

You can clean the files which you don’t want.

This application requires some logic for CRUD operation. So let’s create our contexts, reducers, and custom hooks so that we can use them. These utility functions will allow us to maintain our application state and help us to pass our data dynamically.

Application State Mangement

Context

At first, we will build our todo context so that we can easily pass our props down the order where it is necessary. Inside the context folder create a todos.context.jsfile. This is the only application context we will create in this entire application.

import React, { createContext } from "react";
import { useLocalStorageReducer } from "../hooks/useLocalStorageReducer";
import todoReducer from "../reducers/todo.reducer";

const defaultTodos = [
  { id: 1, task: "Mow the lawn using goats", completed: false },
  { id: 2, task: "Release lady bugs into garden", completed: true },
];
export const TodosContext = createContext();
export const DispatchContext = createContext();

export function TodosProvider(props) {
  const [todos, dispatch] = useLocalStorageReducer(
    "todos",
    defaultTodos,
    todoReducer
  );
  return (
    <TodosContext.Provider value={todos}>
      <DispatchContext.Provider value={dispatch}>
        {props.children}
      </DispatchContext.Provider>
    </TodosContext.Provider>
  );
}

If you look at the code above, you can see the context logic for our application. We are passing some default todo items. The todoReducer will allow us to manipulate the todo form by creating a todo, editing the todos, and deleting them. Whereas the useLocalStorageReducer allows us to store our pieces of information.

Hooks

In our custom hooks, we will be creating some custom hooks for managing our application state that will keep changing at regular intervals. Our application will have three custom hooks, i.e. ‘useLocalStorageReducer.js’, ‘useInputState.js’, and ‘useToggleState.js’. Each custom hook will have its own use case in our application.

Let us create our first custom hook useInputState.js

import { useState } from "react";
export default initialVal => {
  const [value, setValue] = useState(initialVal);
  const handleChange = e => {
    setValue(e.target.value);
  };
  const reset = () => {
    setValue("");
  };
  return [value, handleChange, reset];
};

The useInputState.js hook will allow us to enter the form data of our application. We can also reset the value of our form input.

Our next custom hook is the useLocalStorageReducer.js. This customs hook provides us with the capability to store the new or updated to-do items in the local storage of our web browser.

import { useReducer, useEffect } from "react";
function useLocalStorageReducer(key, defaultVal, reducer) {
  const [state, dispatch] = useReducer(reducer, defaultVal, () => {
    let value;
    try {
      value = JSON.parse(
        window.localStorage.getItem(key) || String(defaultVal)
      );
    } catch (e) {
      value = defaultVal;
    }
    return value;
  });
  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]);
  return [state, dispatch];
}
export { useLocalStorageReducer };

Our last and final hook is useToggleState.js

jimport { useState } from "react";
function useToggle(initialVal = false) {
  // call useState, "reserve piece of state"
  const [state, setState] = useState(initialVal);
  const toggle = () => {
    setState(!state);
  };
  // return piece of state AND a function to toggle it
  return [state, toggle];
}
export default useToggle;

The use of toggle custom hook is that it returns us with the piece of application state and a function to toggle it.

Reducer

We will use todo.reducer.js it is for our reducer. Note that we are the UUID module to create a unique id for each of our to-do lists. You can install the UUID with the following command in your terminal. npm i uuid

import * as uuid from "uuid";
const reducer = (state, action) => {
  switch (action.type) {
    case "ADD":
      return [...state, { id: uuid.v4(), task: action.task, completed: false }];
    case "REMOVE":
      return state.filter(todo => todo.id !== action.id);
    case "TOGGLE":
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    case "EDIT":
      return state.map(todo =>
        todo.id === action.id ? { ...todo, task: action.newTask } : todo
      );
    default:
      return state;
  }
};
export default reducer;

Building The UI

Now our setup is complete, let’s create our React Component, i.e TodoApp.Js. This will be our main application component where our other component will reside for UI rendering.

import React from "react";
import TodoList from "./TodoList";
import TodoForm from "./TodoForm";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import Grid from "@material-ui/core/Grid";
import { TodosProvider } from "./context/todos.context";
function TodoApp() {
  return (
    <Paper
      style={{
        padding: 0,
        margin: 0,
        height: "100vh",
        backgroundColor: "#fafafa"
      }}
      elevation={0}
    >
      <AppBar color='primary' position='static' style={{ height: "64px" }}>
        <Toolbar>
          <Typography color='inherit'>React ToDo</Typography>
        </Toolbar>
      </AppBar>
      <Grid container justify='center' style={{ marginTop: "1rem" }}>
        <Grid item xs={11} md={8} lg={4}>
          <TodosProvider>
            <TodoForm />
            <TodoList />
          </TodosProvider>
        </Grid>
      </Grid>
    </Paper>
  );
}
export default TodoApp;

This component will act as a container for our application. You can consider this to-do component as a Parent element. Our component will display of todo application which will render the other components.

Todo.js Component

import React, { useContext, memo } from "react";
import useToggleState from "./hooks/useToggleState";
import EditTodoForm from "./EditTodoForm";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItem";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction";
import { DispatchContext } from "./context/todos.context";
function Todo({ id, task, completed }) {
  const dispatch = useContext(DispatchContext);
  const [isEditing, toggle] = useToggleState(false);
  // console.log("RE-RENDER ==> ", task);
  return (
    <ListItem style={{ height: "64px" }}>
      {isEditing ? (
        <EditTodoForm id={id} task={task} toggleEditForm={toggle} />
      ) : (
        <>
          <Checkbox
            tabIndex={-1}
            checked={completed}
            onClick={() => dispatch({ type: "TOGGLE", id: id })}
          />
          <ListItemText
            style={{ textDecoration: completed ? "line-through" : "none" }}
          >
            {task}
          </ListItemText>
          <ListItemSecondaryAction>
            <IconButton
              aria-label="Delete"
              onClick={() => dispatch({ type: "REMOVE", id: id })}
            >
              <DeleteIcon />
            </IconButton>
            <IconButton aria-label="Edit" onClick={toggle}>
              <EditIcon />
            </IconButton>
          </ListItemSecondaryAction>
        </>
      )}
    </ListItem>
  );
}
export default memo(Todo);

The Todo.js component is our main call to action component where most of the application logic will reside.

Our next component will be todoList.js

import React, { useContext } from "react";
import Todo from "./Todo";
import Paper from "@material-ui/core/Paper";
import List from "@material-ui/core/List";
import Divider from "@material-ui/core/Divider";
import { TodosContext } from "./context/todos.context";
function TodoList() {
  const todos = useContext(TodosContext);
  if (todos.length)
    return (
      <Paper>
        <List>
          {todos.map((todo, i) => (
            // To add a key to a fragment, we have to use the long-hand version
            // rather than <> </>, we have to use <React.Fragment>
            <React.Fragment key={i}>
              <Todo {...todo} key={todo.id} />
              {i < todos.length - 1 && <Divider />}
            </React.Fragment>
          ))}
        </List>
      </Paper>
    );
  return null;
}
export default TodoList;

Our TodoList component will return the to-do list items that we create. Whenever we create a new todo item, this react todo application component will render those new lists.

TodoForm.js component

import React, { useContext } from "react";
import TextField from "@material-ui/core/TextField";
import Paper from "@material-ui/core/Paper";
import useInputState from "./hooks/useInputState";
import { DispatchContext } from "./context/todos.context";
function TodoForm() {
  const [value, handleChange, reset] = useInputState("");
  const dispatch = useContext(DispatchContext);
  return (
    <Paper style={{ margin: "1rem 0", padding: "0 1rem" }}>
      <form
        onSubmit={e => {
          e.preventDefault();
          dispatch({ type: "ADD", task: value });
          reset();
        }}
      >
        <TextField
          value={value}
          onChange={handleChange}
          margin="normal"
          label="Add New Todo"
          fullWidth
        />
      </form>
    </Paper>
  );
}
export default TodoForm;

This component allows the user to enter values in the todo form.

Our final todo component is EditTodoForm.js

import React, { useContext } from "react";
import useInputState from "./hooks/useInputState";
import TextField from "@material-ui/core/TextField";
import { DispatchContext } from "./context/todos.context";
function EditTodoForm({ id, task, toggleEditForm }) {
  const dispatch = useContext(DispatchContext);
  const [value, handleChange, reset] = useInputState(task);
  console.log("EDIT FORM RENDER!!!");
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        dispatch({ type: "EDIT", id: id, newTask: value });
        reset();
        toggleEditForm();
      }}
      style={{ marginLeft: "1rem", width: "50%" }}
    >
      <TextField
        margin="normal"
        value={value}
        onChange={handleChange}
        fullWidth
        autoFocus
      />
    </form>
  );
}
export default EditTodoForm;

This component will allow us to edit the to-do form.

Now at last we will display all the application UI in our main App.js file.

jimport React from "react";
import TodoApp from "./TodoApp";
function App() {
  return <TodoApp />;
}
export default App;

And that’s it, guys. Here we have a fully functional React Todo Application with modern React practices. And hope you guys will like this simple react application.

How To Run This Project

It’s very simple to run this project. Just follow the steps.

  • Make sure you have node.js installed on your system if not then download it first.
  • After that, you open the project in your favorite code editor(VS Code is preferable).
  • If you are using Vs Code then, simply open the code editor and open your project.
    • Then open a new terminal (ctrl + shift + ~ ==> will open a new terminal).
    • run this command `npm install`
    • then `npm start`
  • If you want to directly run without a code editor, then follow these steps:
    • open a terminal(command prompt or git bash)
    • goto your project root directory(cd folder name)
    • then again type `npm install` and then after installation `npm start`

In case npm gives you any kind of error then you can run the project with the Yarn package manager instead. Follow these steps:

  • Open your windows terminal
  • the type `npm i –global yarn`. This will install your yarn package manager globally.
  • After the installation, repeat the same commands from the above but this time uses yarn instead of npm.
  • `yarn install` and then to start `yarn start`.

DOWNLOAD SIMPLE TODO APPLICATION WITH REACT AND MATERIAL UI CLICK THE DOWNLOAD BUTTON BELOW.


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.