Build a ToDo App With React and Firebase

Thomas Sentre - Mar 2 '23 - - Dev Community

In today's fast-paced world, keeping track of our daily tasks and responsibilities is more important than ever. Whether you're a student, a professional, or a homemaker, having a reliable task management system can help you stay organized and focused on what matters most. One way to achieve this is by using a ToDo application.

In this article, I will guide you through the process of building a ToDo list app using React, with the data stored in the backend, specifically in a Firebase Firestore database.

The ToDo List app will allow users to enter a Todo item and store it in a lovely list in the firebase database. So, this list will be permanent and won’t be changed after a refresh of the browser. Users can also delete it.

The topics covered in this article include the following:

  • Using a React useRef hook.
  • Storing data in a Firebase Firestore database.
  • Using MUI icons and components
  • And much more.

Overview

The final result will look like this:

Todo App Overview

Project folders structure

As we go through the implementation of our application, we will end up with the following folder structure containing files:

Folder structure

Initializing React application

To get started, use the create-react-app command to create a new app called todo-app. Specifically, the command for this is as follows:



npx create-react-app todo-app


Enter fullscreen mode Exit fullscreen mode

Initializing Firebase

Before setting up Firebase, let us talk a bit about it.

Firebase is a set of tools developed by Google that provided backend service. It contains a variety of services such as:

  • Cloud Firestore: Real-time, cloud-hosted, NoSQL database
  • Cloud storage: Massively scalable file storage
  • Cloud functions: Serverless, event-driven back-end functions
  • Authentication: User login and identity
  • Firebase hosting: Global web hosting
  • ML Kit: An SDK for common machine learning tasks

Firebase makes life so easy for Front-end developers to integrate a backend into their application without creating any API routes or other backend code.

So, we will use the cloud Firestore to store data in it.
Now go to Firebase website and create a new account. Create a new project by clicking add project. After this click on Continue to the console button. Scroll down and click the config radio button and then copy all the data for the firebase config section.

Now, inside the src folder, create a file called firebase.js and add paste the copied code to it. This code will look like this:

firebase.js



const firebaseConfig = {
    apiKey: "long-random-key",
    authDomain: "default-domain-of-your-app",
    projectId: "name-project -id",
    storageBucket: "storage-bucket",
    messagingSenderId: "messageSenderId",
    appId: "random-key",
    measurementId: "random-key"
};


Enter fullscreen mode Exit fullscreen mode

Basic React Setup

Now, we will do basic setup for React. Inside the ToDo app directory delete all unnecessary files. So our index.js and App.js files look like this:

src/index.js



import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render( <
    React.StrictMode >
    <
    App / >
    <
    /React.StrictMode>,
    document.getElementById('root')
);


Enter fullscreen mode Exit fullscreen mode

src/App.js



function App() {
    return ( <
        div className = "App" >
        <
        h2 > TODO List App < /h2> <
        /div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Local ToDo List

After doing the setup in the previous section, we will work on our ToDo app. We will update our App.js file to contain the logic for a basic ToDo list. Here, we are using two state variables: todos and input. We are using the useState hook to declare both of them. todos contains an array containing two strings, and input contains an empty string.

Next, inside the return statement, we use the controlled input of React to update the input of an input box. Next, we have a button and a click event assigned to the button. When we click it, we will run a function called addTodo() that changes the state of todos, with setTodos. Here, it appends the already existing content with the user-typed content.

We are using a form to wrap our input and button, and the button type is submitted. Therefore, if we type anything in the input box and press Enter on the keyboard, it will work.

For creating the user interface of our app, we will use a pre-build components library: MUI.

MUI formerly called Material UI is a React component Library developed by Google in 2014 that offers accessible, robust, production-ready, customizable, and reusable code components for faster web development. It uses grid-based layouts, animations, transitions, padding, and many more. It also specifies a large set of standard icons.

Let’s install it by using this command:



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


Enter fullscreen mode Exit fullscreen mode

All we have said in the previous line is resumed by the following code:



import React, { useState } from 'react';
import { TextField, Button } from '@mui/material';
import './App.css';
function App() {
    const [todos, setTodos] = useState([
        'Create Blockchain App',
        'Create a Youtube Tutorial'
    ]);
    const [input, setInput] = useState('');
    const addTodo = (e) => {
        e.preventDefault();
        setTodos([...todos, input]);
        setInput('')
    };
    return (
        <div className="App">
            <h2> TODO List App</h2>
            <form>
                <TextField id="outlined-basic" label="Make Todo" variant="outlined" style={{ margin: "0px 5px" }} size="small" value={input}
                    onChange={e => setInput(e.target.value)} />
                <Button variant="contained" color="primary" onClick={addTodo}  >Add Todo</Button>
            </form>
            <ul>
                {todos.map(todo => <li>{todo}</li>)}
            </ul>
        </div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Now, run the app in your terminal.



npm run start


Enter fullscreen mode Exit fullscreen mode

It looks like this:

Overview 1

ToDo Components

Next, we will move the ToDo list to a separate component. So, create a file named Todo.js inside a component folder. We will send the separate ToDo to it as a props.

The updated code is shown as:



import React, { useState } from 'react';
import { TextField, Button } from '@mui/material';
import Todo from './components/Todo';
import './App.css';
function App() {
    const [todos, setTodos] = useState([
        'Create Blockchain App',
        'Create a Youtube Tutorial'
    ]);
    const [input, setInput] = useState('');
    const addTodo = (e) => {
        e.preventDefault();
        setTodos([...todos, input]);
        setInput('')
    };
    return (
        <div className="App">
            <h2> TODO List App</h2>
            <form>
                <TextField id="outlined-basic" label="Make Todo" variant="outlined" style={{ margin: "0px 5px" }} size="small" value={input}
                    onChange={e => setInput(e.target.value)} />
                <Button variant="contained" color="primary" onClick={addTodo}  >Add Todo</Button>
            </form>
            <ul>
                {todos.map(todo => <Todo todo={todo} />)}
            </ul>
        </div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Now add the following code into the Todo.js file. We are just using a bunch of mui icons and showing the props called todo. These icons help us to make the list item prettier.

src/components/Todo.js



import { List, ListItem, ListItemAvatar, ListItemText } from '@mui/material';
const Todo = ({ todo }) => {
    return (
        <List className="todo__list">
            <ListItem>
                <ListItemAvatar />
                <ListItemText primary={todo} secondary={todo} />
            </ListItem>
        </List>
    )
};
export default Todo;


Enter fullscreen mode Exit fullscreen mode

Run our app, now it looks like this:

Overview 2

Now, it’s time to hook up Firebase to the project.

We will start setting up Firebase for the backend. For that, install all the necessary dependencies.



npm install firebase


Enter fullscreen mode Exit fullscreen mode

Update our firebase.js file to use the config to initialize the app. After that, we will use Firestore as the database.



import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
...........
............
};
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
export { db }


Enter fullscreen mode Exit fullscreen mode

Now, we will go back to Firebase and click Cloud Firestore and then click the Create database button, as shown in following picture:

create database

On the next screen, select Start in test mode and then click the Next button, as shown:

Firebase test mode

On the next screen, click the Enable button:

Firebase enable
On the next screen, click Start collection, as shown in following picture:

Firebase create collection
It will open the pop-up shown in the following picture.

Create ID
We need to enter todos in the Collection ID field and click the Next button.

On the next screen, fill the Document ID field by clicking Auto ID. Also enter todo in the Field, choose string for the type and add a random text in the value field After that, click the Save button.That will take us back to the main screen.

Adding Firebase to the App

Now, we are going to use the data from Firebase database. So, remove the hard-coded stuff in the useState code for todos. After that, within useEffect, we are calling the collection todos , and then we take the snapshot. In Firebase terms, it is the live data, which we will get instantly. We will then set this data in the todos array, via setTodos().

Also, notice that useEffect has input inside the array. So, any time a todo is added by the user, it will instantly display in our app. Notice that we have changed the way we loop through data, using todos. This is done because we receive the data as an array of objects.

Updated code:
src/App.js



import React, { useState, useEffect } from 'react';
import { TextField, Button } from '@mui/material';
import Todo from './components/Todo';
import { db } from './firebase.js';
import { collection, onSnapshot } from 'firebase/firestore';
import './App.css';
function App() {
    const [todos, setTodos] = useState([]);
    const [input, setInput] = useState('');
    useEffect(() => {
        onSnapshot(collection(db, 'todos'), (snapshot) => {
            setTodos(snapshot.docs.map(doc => doc.data()))
        })
    }, [input]);
    const addTodo = (e) => {
        e.preventDefault();
        setTodos([...todos, input]);
        setInput('')
    };
    return (
        <div className="App">
            <h2> TODO List App</h2>
            <form>
                <TextField id="outlined-basic" label="Make Todo" variant="outlined" style={{ margin: "0px 5px" }} size="small" value={input}
                    onChange={e => setInput(e.target.value)} />
                <Button variant="contained" color="primary" onClick={addTodo}  >Add Todo</Button>
            </form>
            <ul>
                {todos.map(({ todo }) => <Todo todo={todo} />)}
            </ul>
        </div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Now, we will add the functionality so the user can add the ToDo item. For that, we just need to add the input to the collection, using addDoc().

Also, notice that we are adding the server timestamp, while adding a ToDo. We are doing this because we need to order the ToDos in descending order.

The resulted code looks like this:
src/App.js



import React, { useState, useEffect } from "react";
import { TextField, Button } from "@mui/material";
import Todo from "./components/Todo";
import { db } from "./firebase";
import {
  collection,
  onSnapshot,
  serverTimestamp,
  addDoc,
} from "firebase/firestore";
import "./App.css";
function App() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState("");
  useEffect(() => {
    onSnapshot(collection(db, "todos"), (snapshot) => {
      setTodos(snapshot.docs.map((doc) => doc.data()));
    });
  }, [input]);
  const addTodo = (e) => {
    e.preventDefault();
    addDoc(collection(db, "todos"), {
      todo: input,
      timestamp: serverTimestamp(),
    });
    setInput("");
  };
  return <div className="App">............</div>;
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Now, we need to get the ID of each item that we require for the key and also for deleting, which we are going to implement. The key is essential in React for optimization, and we also get a warning in the console.
So, we need to change the structure in which we set the data in setTodos().

src/App.js



import React, { useState, useEffect } from 'react';
import { TextField, Button } from '@mui/material';
import Todo from './components/Todo';
import { db } from './firebase.js';
import { collection, query, orderBy, onSnapshot, addDoc, serverTimestamp } from 'firebase/firestore';
import './App.css';
const q = query(collection(db, 'todos'), orderBy('timestamp', 'desc'));
function App() {
    const [todos, setTodos] = useState([]);
    const [input, setInput] = useState('');
    useEffect(() => {
        onSnapshot(q, (snapshot) => {
            setTodos(snapshot.docs.map(doc => ({
                id: doc.id,
                item: doc.data()
            })))
        })
    }, [input]);
    const addTodo = (e) => {
        e.preventDefault();
        addDoc(collection(db, 'todos'), {
            todo: input,
            timestamp: serverTimestamp()
        })
        setInput('')
    };
    return (
        <div className="App">
            <h2> TODO List App</h2>
            <form>
                <TextField id="outlined-basic" label="Make Todo" variant="outlined" style={{ margin: "0px 5px" }} size="small" value={input}
                    onChange={e => setInput(e.target.value)} />
                <Button variant="contained" color="primary" onClick={addTodo}  >Add Todo</Button>
            </form>
            <ul>
                {todos.map(item => <Todo key={item.id} arr={item} />)}
            </ul>
        </div>
    );
}
export default App;


Enter fullscreen mode Exit fullscreen mode

Delete functionality

Next, we will add the delete functionality, in which we will have to get the ID of the item and call the deleteDoc. The updated code looks like this.

src/Todo.js



import { List, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import { db } from "../firebase.js";
import { doc, deleteDoc } from "firebase/firestore";
const Todo = ({ arr }) => {
  return (
    <List className="todo__list">
      <ListItem>
        <ListItemAvatar />
        <ListItemText primary={arr.item.todo} secondary={arr.item.todo} />
      </ListItem>
      <DeleteIcon
        fontSize="large"
        style={{ opacity: 0.7 }}
        onClick={() => {
          deleteDoc(doc(db, "todos", arr.id));
        }}
      />
    </List>
  );
};
export default Todo;


Enter fullscreen mode Exit fullscreen mode

Now, open your terminal and run the application. Our app is now fully functional, but the design looks awful. So, we need to add some styles.

Aweful design
Create a file called todo.css and imports it to the Todo.js file with the following styles:



.todo__list{
display:flex;
justify-content: center;
align-items: center;
width: 800px;
border: 1px solid lightgray;
margin-bottom: 10px !important;
}


Enter fullscreen mode Exit fullscreen mode

Replace all the styles in the App.css with the following:



.App {
display:grid;
place-items:center;
}


Enter fullscreen mode Exit fullscreen mode

Run the application, now it looks much nicer.

cool design

Conclusion

Now that our Todo application can save its data in Firestore database, we can think about the next phase about making this a real application- namely authenticating our users. But to make this article as simple as possible, we will not implement that functionality. Make it in your way. I would be happy to see the final result.

THANK YOU FOR READING
I hope you found this little article helpful. Please share it with your friends and colleagues. Sharing is caring.

Connect with me on various platforms

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .