Todo App

Create a full-stack To-do App using React and Firebase

giridhar7632@giridhar7632

The combination of React and Firebase can move web applications to a whole new level. In this workshop, you will create an awesome full-stack To-do App using React for front-end and Firebase real-time database as back-end. By the end of this workshop, you will learn about Forms in React and implementing CRUD operations using Firebase.

We are going to build something similar to this. 👇

Final todo app

Check out live demo and source code.

This workshop should take about an hour to complete.

Prerequisites

  • You should have a basic understanding of HTML, CSS, and JavaScript.
  • You should also know some concepts of ES6 features of JavaScript.
  • You should be familiar with the fundamentals of React and React Hooks.
  • It's also good if you have a little bit of knowledge about Firebase. But don't worry, we will cover everything from scratch.

Coding Environment

We will be using Repl.it for building our project. Repl.it is an online code editor where you can code in different languages without any installations.

Fork this starter repl here.

It contains create-react-app and firebase installed. It also contains all the styles required so that we can focus only on React and Firebase.

After forking, press the Run button on the top. If you output something like this, you are good to go.

starter template

Creating a Firebase Project

You can access Firebase with your Google account.

Firebase: Google's Firebase is Backend-as-a-Service(BaaS), which makes us easy to add a back-end to our applications with some knowledge of server-side coding.

We are going to use Firebase Realtime Database as a database for our Todo App.

The Firebase Realtime Database is cloud-hosted. Data is stored as JSON and synchronized in real-time to every connected client. We can store and sync data across all clients in real-time and remains available when your app goes offline.

Open Firebase; after signing in, click on Go to console on the top right corner.

firebase.com

In your console, If you don't have any projects yet, it will be clean and empty. Click on Create Project to add a new project.

Firebase console

Complete all the steps to create a firebase project. Name your project as you wish.

Step-1

After giving a name to your project, click the Continue button to proceed further. Here is an optional step to use Google Analytics for your project, but you won't need it in this workshop. Click on Continue.

Step-2

Finally, hit Create Project.

Step-3

ğŸŽ‰ Bingo!!! Your Firebase project will be created. You should see something like this.

Project created

Click on the Continue button.

Then let's configure our Project with Firebase.

Configuring Project with Firebase

Click on the Web (</>) icon.

Project overview

Register your project with a name.

Register name

Here are your app credentials. This code helps your project connect to your Firebase project.

Firebase config

Make sure you copy your web app's Firebase configuration(firebaseConfig object).

Hop on to the forked Repl and create a new component, Firebase.js inside the src folder.

In the Firebase.js component, import the firebase module, which is already installed, and paste the data you copied from Firebase. Your Firebase.js file will be like this with your API keys.

import firebase from 'firebase' // <–– importing firebase

var firebaseConfig = {
    apiKey: "AIzaSyCFg0consEutzxGmXo5zxcibJ2-ZUkGRps",
    authDomain: "todo-app-9f5a0.firebaseapp.com",
    databaseURL: "https://todo-app-9f5a0.firebaseio.com",
    projectId: "todo-app-9f5a0",
    storageBucket: "todo-app-9f5a0.appspot.com",
    messagingSenderId: "235166947516",
    appId: "1:235166947516:web:bca35a6c27a240d4bad8b9",
    measurementId: "G-JYGLVL9X55"
  }

firebase.initializeApp(firebaseConfig) // <–– initializing firebase
export default firebase

Creating Database

Now let's create a database for our application.

Open the Firebase project console. Click on the Realtime Database in the Develop section on the left sidebar.

Project Sidebar

You will see something like this. :point_down: Click on the Create Database button.

Create Database

Set Start in Test Mode and click on Enable.

Test Mode

Then your Realtime Database will be created and initiated with null(nothing).

Realtime Database

Firebase initially allows anyone to read and write the database for 30 days. You can edit the rules anytime in the Rules tab.

Rules

Now that we have a database let's create a Todo App using the database.

CRUD Operations

CRUD stands for Create, Read, Update, and Delete.

These are the four most basic operations that can be performed with most traditional database systems, and they are the backbone for interacting with any database.

CRUD

If you want to learn more theory about CRUD, check it here

Creating Todo App

Let's create the interface of the Todo App to perform CRUD operations.

Create a new folder inside the src folder and name it as components. All of our Todo App components will be in this folder.

Inputting a Todo

Create a new file in the components folder and name it Input.js. Import React into the component, create the functional component and export it.

import React from 'react'

function Input() {
  {/* code goes here*/}
}
 
export default Input

Also, import the Firebase.js file you created before at the top to use Firebase inside this component.

import firebase from '../Firebase'

Create a div with a class of input inside the component.

import React from 'react'
import firebase from '../Firebase'

function Input() {
  return (
    <div className="input">
      {/* code goes here*/}
    </div>
  )
}

export default Input

Add an input and a button Add Todo with a class of add-btn inside the div element.

<div className="input">
  <input type="text" placeholder="Enter a Todo..." />
  <button className="add-btn">
  <i className="fa fa-plus-circle" aria-hidden="true"></i><span id="btn-text">Add Todo</span> 
  </button>
</div>

Here, <i className="fa fa-plus-circle"> is the plus icon from Font Awesome for styling. In the starter template, Font Awesome cdn was already linked in public/index.html file.

Form elements like <input>, <select> and <textarea> in React are slightly different from those of HTML. In HTML, they responsible on their own to handle the user input and update their respective values. But in React, the forms are controlled by components using state. Learn more about React forms here.

Create a state value and update it every time the input changes using the handleChange function.

import React, { useState } from 'react' // <–– importing useState hook
import firebase from '../Firebase'

function Input() {
  const [task, setTask] = useState('') // <–– creating state variable

  function handleChange(e) {
    setTask(e.target.value) // <–– updating the state value
  }

  return (
    <div className="input">
      <input type="text" placeholder="Enter a Todo..." value={task} onChange={handleChange} />
      <button className="add-btn">
      <i className="fa fa-plus-circle" aria-hidden="true"></i><span id="btn-text">Add Todo</span> 
      </button>
    </div>
  )
}

export default Input

Create operation

Let's add the inputted data into the database when the user clicks the Add Todo button.

Add a onClick attribute which invokes handleClick function.

<button className="add-btn" onClick={handleClick} >
  <i className="fa fa-plus-circle" aria-hidden="true"></i><span id="btn-text">Add Todo</span> 
</button>

Now let's write the handleClick function.

Data in Firebase is structured as collections that we can access through references.

function handleClick() {
  const todoRef = firebase.database().ref('todo') // <—— creating a reference in database
  // code goes here
 }

To add data to the database, you can use set() to save data to a specified reference, replacing any existing data at that path.

We use the push() method to append data to a list in multiuser applications. The push() method generates a unique key every time a new child is added to the specified Firebase reference.

More Info: By using these auto-generated keys for each new element in the list, several clients can add children to the same location simultaneously without writing conflicts. The unique key generated by push() is based on a timestamp, so list items are automatically ordered chronologically.

⚠ Note: Using set() overwrites data at the specified location, including any child nodes.

function handleClick() {
  const todoRef = firebase.database().ref('todo')  
  const newTodoRef = todoRef.push()
  const todo = {
    task,
    completed: false
  }
  newTodoRef.set(todo)
  setTask("")             
}

Here 👆, we added the data that we get as input into the database. By default, the task is incomplete. After the data is added to the database, the input field is cleared by setting task to an empty string(setTask("")).

Your final code in Input.js should look like this.
import React, { useState } from 'react'
import firebase from '../Firebase'

function Input() {
  const [task, setTask] = useState('')

  function handleChange(e) {
    setTask(e.target.value)
  }
  function handleClick() {
    const todoRef = firebase.database().ref('todo')  
    const newTodoRef = todoRef.push()
    const todo = {
      task,
      completed: false
    }
    newTodoRef.set(todo)
    setTask("")
  }

  return (
    <div className="input">
      <input type="text" placeholder="Enter a Todo..." value={task} onChange={handleChange} />
      <button className="add-btn" onClick={handleClick}>
      <i className="fa fa-plus-circle" aria-hidden="true"></i><span id="btn-text">Add Todo</span> 
      </button>
    </div>
  )
}

export default Input

Now import Input.js into App.js and check your output by adding some data.

import React from 'react'
import Input from './components/Input' // <—— importing Input.js
import './App.css'

function App() {
  return (
    <div className="App">
      <h1>Todo App</h1>
      <Input />
    </div>
  );
}

export default App;

Output with input

Looks good, doesn't it!

Database

The data you added through the input will be appended to our Firebase Database!

Creating Todo List

Create a new component, TodoList.js, in the components folder. Import React, Firebase, create and export a functional component.

import React, { useState, useEffect } from 'react'
import firebase from '../Firebase'

function TodoList() {
  {/*code goes here*/}
  return (
    <div className="todo-list">

    </div>
  )
}

export default TodoList

Read operation

We will then read the data in the database, fetch it into an array, and display it on the screen.

Create a state variable todoList. This will be the empty array to which we add the data fetched from the database.

 const [todoList, setTodoList] = useState([]) // <—— state variable 

We will read the data only once after the component is rendered. To do that, we use the useEffect hook.

useEffect(() => {
  // code goes here
}, [])

We have our data stored in a specified reference(location).

To read data at a path and listen for changes, we will use the on() method of firebase.database().ref() to observe events. The on() method is used to synchronize data in real-time.

useEffect(() => {
  const todoRef = firebase.database().ref('todo')
  // Syncronizing
  todoRef.on()

}, [])

The on() method takes two parameters:

1) The event type - how you control the level of synchronization from the real-time database. 2) The callback function - the event type above controls the callback function.

Here, we are going to use the value event.

EventTypical usage
valueRead and listen for changes to the entire contents of a path.

The callback function is called every time there is a change at the specified location in the database. The event callback is passed a snapshot containing all data at that location, including child data. If there is no data, the snapshot will return null when you call val() on it.

todoRef.on("value", (snapshot) => {
  const todos = snapshot.val() // <—— value of the data snapshot
})

⭐ Important: The value event is called every time data is changed at the specified database reference, including changes to children. To limit the size of your snapshots, add the event listener only at the lowest level needed for watching changes. For example, adding the listener to the root of your database is not recommended.

Now we need the data as an array to put it into the state. Declare an empty array and add the id and task into the array.

todoRef.on("value", (snapshot) => {
  const todos = snapshot.val()
  const todoList = []
  for(let id in todos) {
    todoList.push({id,...todos[id]}) // <—— adding todos to todoList
  }
  setTodoList(todoList) // <—— setting the state
})

Now that we have the data in todoList, we can map through it to display it.

Initially, it is an empty array. So we have to check whether it is empty or not, then map through it.

For now, let's display our list using the <h1> tag but later, we will create a separate component for rendering it.

return (
  <div className="todo-list">
    {todoList ? todoList.map((todo) => <h1> {todo.task} </h1>) : null}
  </div>
)
Your TodoList component's code so far:
import React, { useState, useEffect } from 'react'
import firebase from '../Firebase'

function TodoList() {
  const [todoList, setTodoList] = useState([])

  useEffect(() => {
    const todoRef = firebase.database().ref('todo')
    // Syncronizing
    todoRef.on("value", (snapshot) => {
      const todos = snapshot.val()
      const todoList = []
      for(let id in todos) {
        todoList.push({id,...todos[id]})
      }
      setTodoList(todoList)
    })

  }, [])
  
  return (
    <div className="todo-list">
      {todoList ? todoList.map((todo) => <h1> {todo.task}</h1>) : null}
    </div>
  )
}

export default TodoList

This will display the data that is retrieved from the database. Make sure you import and render the TodoList component into App.js.

Retrived data

So far, we have done Create and Read operations. Halfway done!!! 🙌

Half way done

Let's continue to work with Update and Delete operations.

Creating Todo Item

Create another file, Todo.js, in the components folder. Import React and Firebase. Also, create a functional component that takes a prop todo.

import React from 'react'
import firebase from '../Firebase'

function Todo({ todo }) {
  return (
    <div classsName="todo-item">

    </div>
  )
}

export default Todo

Update the TodoList by importing Todo and rendering it.

return (
  <div className="todo-list">
    {todoList ? todoList.map((todo, i) => <Todo todo={ todo } key={ i } />) : null}
  </div>
)

Here, 👆 we are rendering a Todo component for each todo and also passing the data to that component as prop, using map() method. Learn more about map() here. If you want to know more about rendering multiple components, check this.

Your final TodoList component looks like this:
import React, { useState, useEffect } from 'react'
import firebase from '../Firebase'
import Todo from './Todo'

function TodoList() {
  const [todoList, setTodoList] = useState([])

  useEffect(() => {
    const todoRef = firebase.database().ref('todo')
    // Syncronizing data
    todoRef.on("value", (snapshot) => {
      const todos = snapshot.val()
      const todoList = []
      for(let id in todos) {
        todoList.push({id,...todos[id]})
      }
      
      setTodoList(todoList)
    })

  }, [])
  
  return (
    <div className="todo-list">
      {todoList ? todoList.map((todo, i) => <Todo todo={ todo } key={ i } />) : null}    {/* <—— updated with 
      <Todo />*/}
    </div>
  )
}

export default TodoList

Hop on to the Todo component, and let's display the data.

function Todo({ todo }) {
  return (
    <div className="todo-item">
      <div className="task">
        <input type="checkbox" />
        <p>{todo.task}</p>
      </div>
      <div className="buttons">
        <button className="del-btn"><i className="fa fa-trash" aria-hidden="true"></i></button>
      </div>
    </div>
  )
}

This displays the todo items with a checkbox and delete button.

Let's add functionality to the buttons.

Update operation

When the checkbox is checked, the task is completed. Let's update the value of completed to true.

Add the onClick attribute to the checkbox that invokes completeTodo function. Whether the checkbox is checked or not depends on the completed value.

<input type="checkbox" onClick={ completeTodo } checked={ todo.completed }/>

To simultaneously write to specific children of a node without overwriting other child nodes, we can use the update() method. When we call update(), it updates lower-level child values by specifying a path for the key.

function completeTodo() {
  const todoRef = firebase.database().ref('todo')
  todoRef.child(todo.id).update({ // <—— updates the object with the key of todo.id
    completed: !todo.completed
  })
}

The above code updates the completed in the database.

Click on Run and check whether it's working.

completed task

Also, let's add styling for the completed tasks before our completeTodo() function.

const completedStyle = { // <—— styles for completed task
    fontStyle: "italic",
    opacity: 0.4,
    textDecoration: "line-through"
}

And apply that style to the paragraph containing our task.

<p style={todo.completed ? completedStyle : null} >{todo.task}</p>
Our code so far:
import React from 'react'
import firebase from '../Firebase'

export default function Todo({ todo }) {
  const completedStyle = { // <—— styles for completed task
    fontStyle: "italic",
    opacity: 0.4,
    textDecoration: "line-through"
   }

  function completeTodo() {
    const todoRef = firebase.database().ref('todo')
    todoRef.child(todo.id).update({
      completed: !todo.completed
    })
  }
  return (
    <div className="todo-item">
      <div className="task">
        <input type="checkbox" onClick={ completeTodo }checked={todo.completed}/>
        <p style={todo.completed ? completedStyle : null} >{todo.task}</p> {/* <—— adding className depending on todo.completed */}
      </div>
      <div className="buttons">
        <button className="del-btn"><i className="fa fa-trash" aria-hidden="true"></i></button>
      </div>
    </div>
  )
}

Now our completed tasks will be styled accordingly.

struck off

Let's add functionality to our delete button.

Delete operation

Add onClick attribute to the button with a deleteTodo function.

<button className="del-btn" onClick={ deleteTodo }>
  <i className="fa fa-trash" aria-hidden="true"></i>
</button>

The simplest way to delete data is to call remove() on a reference to that data location.

function deleteTodo() {
  const todoRef = firebase.database().ref('todo')
  todoRef.child(todo.id).remove()
}
Our final Todo component will be something like this:
import React from 'react'
import firebase from '../Firebase'

function Todo({ todo }) {
  const completedStyle = {
    fontStyle: "italic",
    opacity: 0.4,
    textDecoration: "line-through"
  }
  
  function completeTodo() {
    const todoRef = firebase.database().ref('todo')
    todoRef.child(todo.id).update({
      completed: !todo.completed
    })
  }
  function deleteTodo() {
    const todoRef = firebase.database().ref('todo')
    todoRef.child(todo.id).remove()
  }

  return (
    <div className="todo-item">
      <div className="task">
        <input type="checkbox" onClick={ completeTodo }checked={todo.completed}/>
        <p style={todo.completed ? completedStyle:null} >{todo.task}</p>
      </div>
      <div className="buttons">
        <button className="del-btn" onClick={ deleteTodo }><i className="fa fa-trash" aria-hidden="true"></i></button>
      </div>
    </div>
  )
}

export default Todo

Delete todo

That's it, folks! We completed our Full-stack Todo App. Check your final code here. ✌

Completed

Hacking

Now it's your turn to customize.

  • Go wild with the styles. Create your custom Todo Apps.
  • Add more functionality. Eg:- chance to edit the added todo, alert before deleting any todo, etc.
  • Filter the tasks as completed and due.

I recommend customizing this project as much as you can! Share your creation with everyone in the #ship channel on the Hack Club slack. Don't forget to share it with me too!!! I'm @Giridhar on slack. I'd love to hear from you.

Inspiration

These are some examples to inspire your thoughts.

Edit this page on GitHub