This tutorial is about making a simple Todo list using React, Firebase (Authentication and Realtime Database), React Hooks, and React Bootstrap.

You can download the code on GitHub.

The App we’re building

Getting Started

First thing to do is open up a Terminal and create a new React App:

npx create-react-app todo-list 

Go to your new /todo-list/ directory and use this to install the dependencies:

yarn add firebase react-bootstrap bootstrap

The React part is done for now. Go to Firebase and add a new project. Activate Email and Password Authentication, and Realtime Database. Set up the database with the following rules:

{
  "rules": {
    ".write": "false",
    ".read": "false",
      "users": {
      "$uid": {
        ".write": "$uid === auth.uid",
        ".read": "$uid === auth.uid"
      }
    }
  }
}

This allows only the logged-in user to access their Todo List. Each user has their own list, and the $uid === auth.uid lines mean “if the ID of the data matches the ID of the logged-in user, give them read and write access.”

Next create a Web App via Project Settings and get your initialization code.

Firebase and .env.local

In your main directory, create a file called .env.local and paste in this code, substituting the values with the ones in your initialization code. Do not use quotation marks. .env.local is a good way to increase security and helps you reuse code blocks.

REACT_APP_FIREBASE_API_KEY=YOUR_VALUE
REACT_APP_FIREBASE_AUTH_DOMAIN=YOUR_VALUE
REACT_APP_FIREBASE_PROJECT_ID=YOUR_VALUE
REACT_APP_FIREBASE_STORAGE=YOUR_VALUE
REACT_APP_FIREBASE_MESSAGING_SENDER=YOUR_VALUE
REACT_APP_FIREBASE_APP_ID=YOUR_VALUE
REACT_APP_FIREBASE_MEASUREMENT_ID=YOUR_VALUE

Back to React, create a new file called firebase.js under your /src/ directory. Copy and paste this there. It uses the values we set up earlier from .env.local.

// firebase.js

import { initializeApp } from "firebase/app";

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
  measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID
};

const app = initializeApp(firebaseConfig);

Now open your App.js and replace it with the following:

// App.js

import React, { useState, useEffect } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';

import { Container } from 'react-bootstrap';
import { getAuth } from "firebase/auth";

import Login from './components/Login';
import TodoList from './components/TodoList';

import './firebase';

function App() {

  const [currentUser, setCurrentUser] = useState(false);

  useEffect(() => {
    getAuth().onAuthStateChanged(function (user) {
      if (user) {
        setCurrentUser(user);
      } else {
        setCurrentUser(false)
      }
    });
  }, [getAuth().currentUser])

  return (
    <Container style={{ maxWidth: 800 }}>
      <h1>React Firebase Todo List Tutorial</h1>
      {
        currentUser ?
          <TodoList />
          :
          <Login />
      }
    </Container>
  );
}

export default App;

Make a folder under /src/ called /components/ and make two files in there: Login.js and Todolist.js. Inside Login.js put:

// Login.js

function App() {
  return(
    <p>Login</p>
  )
}

export default App;

And inside TodoList.js put:

// TodoList.js

function App() {
  return(
    <p>TodoList</p>
  )
}

export default App;

Run the app now and make sure everything is working. You’ll see a very simple screen with a header and “Login.” We now have the basis of our application.

Authentication

Let’s look at the App.js file for the first part of Authentication.

const [currentUser, setCurrentUser] = useState(false);

  useEffect(() => {
    getAuth().onAuthStateChanged(function (user) {
      if (user) {
        setCurrentUser(user);
      } else {
        setCurrentUser(false)
      }
    });
  }, [getAuth().currentUser])

The first line uses the useState() hook and it’s where we’re going to store our user information from Firebase. The useEffect() hook runs on load, and reloads every time there is a change in user. The last line, [getAuth().currentUser] is what sets the dependency for the reload. The hook itself checks for any changes in authentication state (i.e. user logs in or out), and sets the currentUser object.

In the body of the App.js file you see this:

{
  currentUser ?
    <TodoList />
    :
    <Login />
}

This is checking to see if a currentUser object exists (i.e. is the user logged in or not). If it does, it shows the TodoList component, otherwise it shows the Login component. Since we haven’t added any authentication yet we see the Login message when we run the App.

Register and Login

Open up your Login.js file and paste in the following:

// components/Login.js

import React, { useState, useRef } from 'react';

import { Form, Button, Alert } from 'react-bootstrap';
import { getAuth, createUserWithEmailAndPassword, signInWithEmailAndPassword } from "firebase/auth";

function App() {

  const emailRef = useRef();
  const passwordRef = useRef();
  const email = useRef();
  const password = useRef();
  const [registerError, setRegisterError] = useState('');
  const [loginError, setLoginError] = useState('');

  async function login() {
    setRegisterError('');
    setLoginError('');
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email.current?.value, password.current?.value)
      .then((userCredential) => {
        // Signed in
      })
      .catch((error) => {
        const errorMessage = error.message;
        setLoginError(errorMessage);
      });
  }
  async function registerUser() {
    setRegisterError('');
    setLoginError('');
    const auth = getAuth();
    createUserWithEmailAndPassword(auth, emailRef.current?.value, passwordRef.current?.value)
      .then((userCredential) => {
        // Signed in 
      })
      .catch((error) => {
        const errorMessage = error.message;
        setRegisterError(errorMessage);
      });
  }


  return (
    <div>
      <h2>Register</h2>
      {
        registerError ?
          <Alert>{registerError}</Alert>
          :
          ""
      }
      <Form>
        <Form.Group className="mb-3" controlId="formBasicEmail">
          <Form.Label>Email address</Form.Label>
          <Form.Control type="email" placeholder="Enter email" ref={emailRef} />
        </Form.Group>

        <Form.Group className="mb-3" controlId="formBasicPassword">
          <Form.Label>Password</Form.Label>
          <Form.Control type="password" placeholder="Password" ref={passwordRef} />
        </Form.Group>
        <Button variant="primary"
          onClick={() => registerUser()}>
          Submit
        </Button>
      </Form>

      <h2 className="mt-4">Login</h2>
      {
        loginError ?
          <Alert>{loginError}</Alert>
          :
          ""
      }
      <Form>
        <Form.Group className="mb-3" controlId="formBasicEmail">
          <Form.Label>Email address</Form.Label>
          <Form.Control type="email" placeholder="Enter email" ref={email} />
        </Form.Group>

        <Form.Group className="mb-3" controlId="formBasicPassword">
          <Form.Label>Password</Form.Label>
          <Form.Control type="password" placeholder="Password" ref={password} />
        </Form.Group>
        <Button variant="primary"
          onClick={() => login()}>
          Submit
        </Button>
      </Form>
    </div>
  );
}

export default App;

That’s a lot so we’ll go through it one step at a time. We are using React Bootstrap Forms. The first thing to do is define some constants:

const emailRef = useRef(); // registration email
const passwordRef = useRef(); // registration password
const email = useRef(); // login email
const password = useRef(); // login password
const [registerError, setRegisterError] = useState(''); // registration error message
const [loginError, setLoginError] = useState(''); // login error message

Next are the functions:

async function login() {
    setRegisterError('');
    setLoginError('');
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email.current?.value, password.current?.value)
      .then((userCredential) => {
        // Signed in
      })
      .catch((error) => {
        const errorMessage = error.message;
        setLoginError(errorMessage);
      });
  }
  async function registerUser() {
    setRegisterError('');
    setLoginError('');
    const auth = getAuth();
    createUserWithEmailAndPassword(auth, emailRef.current?.value, passwordRef.current?.value)
      .then((userCredential) => {
        // Signed in 
      })
      .catch((error) => {
        const errorMessage = error.message;
        setRegisterError(errorMessage);
      });
  }

Both functions clear the error messages, and uses Firebase functions to register and login. When you see email.current?.value, this is referencing the email useRef() we used earlier. Once either of these is successful, it will send a message to our App.js saying that a user has logged in.

You can now run the App and you should be able to register and login to see the Todo List message. If you are having trouble check your console to make sure you’re connecting to Firebase properly.

Todo List

Now that we’re logged in we can work on the Todo List component. Paste this into your TodoList.js file:

// TodoList.js

import React, { useState, useEffect, useRef } from 'react';

import { Form, Button, ListGroup } from 'react-bootstrap';
import { getAuth } from "firebase/auth";
import { getDatabase, ref, push, onValue, update } from "firebase/database";

function App() {

  const [currentUser, setCurrentUser] = useState(false);
  const [todos, setTodos] = useState([]);
  const [todoKeys, setTodoKeys] = useState([]);
  const todoRef = useRef();
  const [toEdit, setToEdit] = useState('');
  const [editingTodo, setEditingTodo] = useState('');

  function addTodo() {
    const db = getDatabase();
    push(ref(db, 'users/' + currentUser.uid + '/todos'), {
      todo: todoRef.current?.value,
      show: true
    });
  }

  function deleteTodo(todoID) {
    const db = getDatabase();
    update(ref(db, 'users/' + currentUser.uid + '/todos/' + todoID), {
      show: false
    });
  }

  function editTodo() {
    const db = getDatabase();
    update(ref(db, 'users/' + currentUser.uid + '/todos/' + toEdit), {
      todo: editingTodo
    });
    setToEdit('');
  }

  function logout() {
    getAuth().signOut();
  }

  function openEdit(todoID) {
    setToEdit(todoID);
    setEditingTodo(todos[todoID].todo)
  }

  useEffect(() => {
    getAuth().onAuthStateChanged(function (user) {
      if (user) {
        setCurrentUser(user);
        // User is signed in.
      } else {
        // No user is signed in.
        setCurrentUser(false)
      }
    });
  }, [getAuth().currentUser])

  useEffect(() => {
    if (currentUser) {
      const db = getDatabase();
      const theRef = ref(db, 'users/' + currentUser.uid + '/todos');
      onValue(theRef, (snapshot) => {
        const data = snapshot.val();
        setTodos(data);
        setTodoKeys(Object.keys(data));
      });
    }
  }, [currentUser])

  return (

    <div>
      <Button
        onClick={() => logout()}>Logout</Button>
      <Form className="mt-2">

        <Form.Group className="mb-3">
          <Form.Label>Todo</Form.Label>
          <Form.Control type="text" placeholder="Enter todo" ref={todoRef} />
        </Form.Group>
        <Button variant="primary"
          onClick={() => addTodo()}>
          Add todo
        </Button>
      </Form>
      <ListGroup className="mt-4">
        {
          todos ?
            todoKeys.map((i) => (
              todos[i].show ?
                <ListGroup.Item key={i}>
                  {
                    i === toEdit ?
                      <Form>
                        <Form.Group className="mb-3">
                          <Form.Control type="text" value={editingTodo} onChange={(e) => setEditingTodo(e.target.value)} />
                        </Form.Group>
                        <Button
                          variant="primary"
                          onClick={() => editTodo()}>
                          Submit
                        </Button>
                      </Form>
                      :
                      <div>{
                        todos[i].todo}
                        <Button
                          variant="danger"
                          style={{ float: "right" }}
                          onClick={() => deleteTodo(i)}>
                          Delete</Button>
                        <Button
                          variant="primary"
                          style={{ float: "right" }}
                          onClick={() => openEdit(i)}>
                          Edit</Button>
                      </div>
                  }
                </ListGroup.Item>
                :
                ""
            ))
            :
            ""
        }
      </ListGroup>
    </div>
  );
}

export default App;

First we define some constants:

const [currentUser, setCurrentUser] = useState(false); // user object
const [todos, setTodos] = useState([]); // list of todos
const [todoKeys, setTodoKeys] = useState([]); // list of todos object keys
const todoRef = useRef(); // reference to the new todo
const [toEdit, setToEdit] = useState(''); // todo ID to be edited
const [editingTodo, setEditingTodo] = useState(''); // the todo being edited

Now the functions:

function addTodo() {
    const db = getDatabase();
    push(ref(db, 'users/' + currentUser.uid + '/todos'), {
      todo: todoRef.current?.value,
      show: true
    });
  }

  function deleteTodo(todoID) {
    const db = getDatabase();
    update(ref(db, 'users/' + currentUser.uid + '/todos/' + todoID), {
      show: false
    });
  }

  function editTodo() {
    const db = getDatabase();
    update(ref(db, 'users/' + currentUser.uid + '/todos/' + toEdit), {
      todo: editingTodo
    });
    setToEdit('');
  }

The Firebase functions mostly work the same. You get a reference to the database, then add, delete, or edit. Under addToDo() you’ll see push which adds a new database entry without disturbing the others. This way we get individual todos with unique keys. Under deleteTodo() and editTodo() is update. This updates one or many values. In deleteTodo() we’re not actually deleting the record, just marking it to not show.

function logout() {
    getAuth().signOut();
  }

  function openEdit(todoID) {
    setToEdit(todoID);
    setEditingTodo(todos[todoID].todo)
  }

There are a couple of other functions. logout() is a call to Firebase’s signOut() so you can get back to the login screen and openEdit() takes the todo’s key and sets up values for editing.

useEffect(() => {
    getAuth().onAuthStateChanged(function (user) {
      if (user) {
        setCurrentUser(user);
        // User is signed in.
      } else {
        // No user is signed in.
        setCurrentUser(false)
      }
    });
  }, [getAuth().currentUser])

  useEffect(() => {
    if (currentUser) {
      const db = getDatabase();
      const theRef = ref(db, 'users/' + currentUser.uid + '/todos');
      onValue(theRef, (snapshot) => {
        const data = snapshot.val();
        setTodos(data);
        setTodoKeys(Object.keys(data));
      });
    }
  }, [currentUser])

The useEffect() functions set up the realtime data. Like on App.js the first sets the currentUser object. The second makes sure there’s a user, then makes a call to the database to get that user’s todos. We set them into the todos constant and extract the keys into todoKeys.


      <ListGroup className="mt-4">
        {
          todos ?
            todoKeys.map((i) => (
              todos[i].show ?
                <ListGroup.Item key={i}>
                  {
                    i === toEdit ?
                      <Form>
                        <Form.Group className="mb-3">
                          <Form.Control type="text" value={editingTodo} onChange={(e) => setEditingTodo(e.target.value)} />
                        </Form.Group>
                        <Button
                          variant="primary"
                          onClick={() => editTodo()}>
                          Submit
                        </Button>
                      </Form>
                      :
                      <div>{
                        todos[i].todo}
                        <Button
                          variant="danger"
                          style={{ float: "right" }}
                          onClick={() => deleteTodo(i)}>
                          Delete</Button>
                        <Button
                          variant="primary"
                          style={{ float: "right" }}
                          onClick={() => openEdit(i)}>
                          Edit</Button>
                      </div>
                  }
                </ListGroup.Item>
                :
                ""
            ))
            :
            ""
        }
      </ListGroup>

The first part of TodoList.js is a form similar to what we used when logging in. The next part is more difficult. We start by making sure there is a todos object, then we map through the todoKeys object. We can’t map through the todos object because it’s an object and not an array. From there we check if we should show the todo (i.e. if it’s marked “show” in the database) and whether or not it’s being edited. If it’s being edited it shows a form.

Finally it will show the todos with buttons to edit and delete.

You can run the App now and have a fully-functional and authenticated Todo List!

Deployment

Here are a couple of quick tips for deploying your site to a server. First open your package.json and define your home page to prevent white screen issues. You can put it right below “name.”

"homepage": "[YOUR URL]"

The other is to create an Upload scripts in your package.json. You need SSH access. Run “yarn build,” then “yarn upload” and off you go.

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "upload": "scp -r ./build/* root@[YOUR IP]:/var/www/[YOUR SITE]"
  }