Code, Deploy, Succeed: Next.js, Firebase, and Tailwind CSS Unleashed

Written by

In this tutorial, we will leverage three key and cutting-edge technologies: Next.js, Firebase, and Tailwind CSS. Our mission? To guide you in building a comprehensive application that demonstrates the capabilities of these tools.

In the hustle and bustle of our daily lives, maintaining organization can be quite challenging. Balancing work, personal commitments, and everything in between often leaves us juggling numerous tasks and responsibilities, making it easy to become overwhelmed and lose track of what needs to be done. This is where a reliable to-do list application becomes an indispensable ally. And that's precisely what we are here to help you build in this tutorial. 

This is what your final product will look like:

Screenshot of the to-do list application, which displays complete and incomplete tasks.

Environment settings

Before diving into coding, you should ensure you have the right environment set up. You will start by installing a package that simplifies the process of creating a basic Next.js application with all the necessary configurations.

1. Get your Next.js application ready

In your terminal, run this command to install the package needed to create Next.js projects:

npx create-next-app

The setup process will prompt you for some project configurations. Here's what you need to use for your app:

  • Project name: my-nextjs-app
  • TypeScript: No
  • ESLint: No
  • Tailwind CSS: Yes
  • src/ directory: No
  • App Router (recommended): Yes 
  • Customize default import alias: No
Screenshot displaying the previously described project configurations.

After providing the initial configurations, you will notice that a new folder has been created. To explore the files inside and run your development server, enter:

npm run dev

Once you launch the development server, you will be able to access your application in your web browser by navigating to http://localhost:3000/.

Screenshot of Next.js, accessed from the localhost:3000 URL.

2. Customize your Next.js application

Upon running the development server, you will be able to see the default page of your Next.js application. This page is managed by the page.js file located in the app/ folder. You will customize this page to display the to-do list application as you progress in your development journey.

Set up a basic HTML template in the page.js file to get started, and replace all main HTML elements with the following code:

app/page.js

export default function Home() {
  return (
    <div className="flex items-center justify-center w-screen h-screen font-medium">
      <div className="flex flex-grow items-center justify-center h-full text-gray-600 bg-gray-100">
        <div className="max-w-full p-8 bg-white rounded-lg shadow-lg w-96">
          <div className="flex items-center mb-6">
            <svg
              className="h-8 w-8 text-indigo-500 stroke-current"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
              />
            </svg>
            <h4 className="font-semibold ml-3 text-lg">Ellen Ripley's tasks</h4>
          </div>

          <div>
            <input className="hidden" type="checkbox" />
            <label className="flex items-center h-10 px-2 rounded cursor-pointer hover:bg-gray-100">
              <span className="flex items-center justify-center w-5 h-5 text-transparent border-2 border-gray-300 rounded-full">
                <svg
                  className="w-4 h-4 fill-current"
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                >
                  <path
                    fillRule="evenodd"
                    d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
                    clipRule="evenodd"
                  />
                </svg>
             </span>
             <span className="ml-4 text-sm">Task</span>
            </label>
         </div>

         <button className="items-center h-8 px-2 mt-2 text-sm font-medium rounded">
            <svg
              className="w-5 h-5 text-gray-400 fill-current"
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              stroke="currentColor"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth="2"
                d="M12 6v6m0 0v6m0-6h6m-6 0H6"
              />
            </svg>
          </button>
          <input
            className="flex-grow h-8 ml-4 bg-transparent focus:outline-none font-medium"
            type="text"
            placeholder="add a new task"
          />
        </div>
      </div>
    </div>
  );
}

Take note that this template already includes many Tailwind CSS class names to streamline styling. For an even smoother development experience, we recommend using the Tailwind CSS IntelliSense Visual Studio extension.

By now, this is what you should see in our local browser:

Screenshot of the to-do list application with only one task.

In globals.css, add these lines to apply styles to the checkbox when it's in the checked state:

app/globals.css

input[type="checkbox"]:checked + label span:first-of-type {
  background-color: #10b981;
  border-color: #10b981;
  color: #fff;
}

input[type="checkbox"]:checked + label span:nth-of-type(2) {
  text-decoration: line-through;
  color: #9ca3af;
}

Development

For this application, you are going to create new to-do tasks that will be stored in Firebase and change their status from incomplete to complete. First, you will create a state for the to-do list to display some demo static information. Later on, you will change it to use real data from a database storage in Firebase.

1. Create a to-do list with static content

The first step is to add code to render a to-do list with static content. Add these lines just before the return statement in the page.js file:

app/page.js

export default function Home() {

  const [items, setItems] = useState([
      { name: "Wake up from hypersleep", status: "complete" },
      { name: "Rescue Newt", status: "complete" },
      { name: "Blew up the planet", status: "incomplete" },
      { name: "Kill the queen", status: "incomplete" },
      { name: "Repair Bishop", status: "incomplete" },
    ]);

 return (
    <div className="flex items-center justify-center w-screen h-screen font-medium">
...

These lines create a state with some static demo tasks to be rendered. Your demo data is an array of objects with two properties: name and status.

Because you are using a state, you must import the useState hook. For that purpose, include these lines at the beginning of the file:

app/page.js

"use client";
import React, { useState } from "react";

export default function Home() {
...

Now, modify the HTML template to render the demo to-do list:

app/page.js

...
  {items.map((item, id) => (
            <div key={id}>
              <input
                className="hidden"
                type="checkbox"
                id={"task_" + id}
                checked={item.status === "complete"}
              />
              <label
                className="flex items-center h-10 px-2 rounded cursor-pointer hover:bg-gray-100"
                htmlFor={"task_" + id}
              >
...
                <span className="ml-4 text-sm">{item.name}</span>
              </label>
            </div>
          ))}
...

In the code above, you are only showing the lines that need to be modified. If you compare this part of the code with the previous one, you will notice that you are basically mapping the items array to show every task, rendering its name, and displaying the checkbox according to its status (complete or incomplete).

At this point, this is what your application will look like:

Screenshot of the to-do list application with completed and pending tasks.

Learn more about how to add interactivity with React state

2. Add a new task to the to-do list

Next, create a function to add a new task just before the return function in page.js:

app/page.js

...

  const [items, setItems] = useState([
    { name: "Wake up from hypersleep", status: "complete" },
    { name: "Rescue Newt", status: "complete" },
    { name: "Blew up the planet", status: "incomplete" },
    { name: "Kill the queen", status: "incomplete" },
    { name: "Repair Bishop", status: "incomplete" },
  ]);

  const [newItem, setNewItem] = useState({ name: "", status: "" });

  // Add
  const addItem = async (e) => {
    e.preventDefault();

    if (newItem.name !== "") {
      setItems([...items, newItem]);
    }
  };

  return (
...

Now, call the addItem function when the button is clicked, and add a listener for the change event to update the input value as a new item in our state. Please note that you have just introduced a couple of functions, setNewItem and newItem, to create a new to-do with an empty name and status by default.

app/page.js

          <button className="items-center h-8 px-2 mt-2 text-sm font-medium rounded" onClick={addItem}>
            <svg
...
            </svg>
          </button>
          <input
            className="flex-grow h-8 ml-4 bg-transparent focus:outline-none font-medium"
            type="text"
            placeholder="add a new task"
            value={newItem.name}
            onChange={(e) => setNewItem({ ...newItem, name: e.target.value })}
          />
...

At this point, you can only add new tasks locally, so your next step will be to set up Firebase to store this data in the cloud.

3. Set up Firebase

You will now create the Firebase application using the Firebase console. You won’t need Google Analytics for this project, so you should disregard the following recommendation: 

Screenshot of Firebase with 'Enable Google Analytics' disabled.

Next, install Firebase locally with this command:

npm install firebase

Once you have your app created, you need to register it and create a file with the Firebase connection. You can get the information from Firebase.

Screenshot of the 'Add Firebase to your web app' registration section.

From Firebase’s “Project Overview” section, access the project settings to get the connection code:

Image showing how to access Firebase project settings from the Project Overview.

You should be able to see the project configuration details:

Screenshot of the project configuration details displayed in Firebase.

Now you can copy the information in a file named firebase.js in the app/ folder of the project. Inside firebase.js, add the following code to get the database from Firebase:

import { getFirestore } from "firebase/firestore";

export const db = getFirestore(app);

Finally, this is what your Firebase connection file should look like:

app/firebase.js

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

import { getFirestore } from "firebase/firestore";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "my-nextjs-app-cb61e.firebaseapp.com",
  projectId: "my-nextjs-app-cb61e",
  storageBucket: "my-nextjs-app-cb61e.appspot.com",
  messagingSenderId: "568914159850",
  appId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

export const db = getFirestore(app);

Since you have already set up Firebase for your project, you can now use it in your code to interact with the Firebase database and store your to-do list data.

4. Import Firebase connection

Next, import the Firebase connection by adding these lines at the beginning of the file:

app/page.js

"use client";
import React, { useState } from "react";
import {
  collection, // Used to get a collection
  addDoc, // Used to add a new document
  query, // To make query of Cloud Firebase
  onSnapshot, // To take pictures
  doc, // Get a document
  runTransaction, // Execute a transaction
} from "firebase/firestore";
import { db } from "./firebase";

5. Modify the ADD function

Here, you will enhance your addItem function to leverage your Firebase integration. This involves using Firebase functions like collection and addDoc since Cloud Firestore organizes data into documents stored within collections.

app/page.js

...
// Add
  const addItem = async (e) => {
    e.preventDefault();

    if (newItem.name !== "") {
      await addDoc(collection(db, "items"), {
        name: newItem.name.trim(),
        status: "incomplete",
      });
    }
  };
...

Learn more about how to add data to Firebase Cloudstore

In the code above you are using your connection to Firebase to add a new document inside of a collection named “items” within your Cloud Firestore. If this collection doesn’t already exist, Firebase will create it for you. By default, every new task in the to-do list will have an “incomplete” status, allowing you to mark it as complete later on.

At this point you should be able to see the new to-dos stored in Cloud Firestore:

Screenshot of Cloud Firestore displaying the tasks that were recently added.

6. Read tasks from Cloud Firebase

The next step is retrieving your to-dos from Cloud Firebase. First, you will import the useEffect hook from React, which is designed for managing side effects in function components. You will use it to implement a task retrieval method and read each task from your to-do list. 

In your app/page.js file, make sure to import useEffect:

app/page.js

...
import React, { useState, useEffect } from "react";
...

With useEffect imported, you can fetch data from Cloud Firebase. To do this, create a query to access a collection named "items" using the query function and pass it to onSnapshot. This function gives you a real-time snapshot of the documents in the collection. Once you have that picture, you get all these documents and create an array called itemsArr, which holds the task data. Finally, set your component's state with this array.

Here's the code:

app/page.js

...
  // Read
  useEffect(() => {
    const q = query(collection(db, "items"));
    const unsubscribe = onSnapshot(q, (querySnapshot) => {
      let itemsArr = [];

      querySnapshot.forEach((doc) => {
        itemsArr.push({ ...doc.data(), id: doc.id });
      });
      setItems(itemsArr);

      return () => unsubscribe();
    });
  }, []);
...  

With this code in place, your application should display the to-do list data retrieved from Cloud Firebase.

Screenshot of the to-do list application with sample pending tasks.

7. Update task status

The last step before moving to deployment is creating a function to update the status of each task in the to-do list. For that purpose, you will create an asynchronous function named updateItem that takes one parameter, id. Check the code below:

app/page.js

// Update
  const updateItem = async (id) => {
    const itemToUpdate = doc(db, "items", id);
    await runTransaction(db, async (transaction) => {
      const sfDoc = await transaction.get(itemToUpdate);
      if (!sfDoc.exists()) {
        throw "Document does not exist!";
      }

      if (sfDoc.data().status === "complete") {
        transaction.update(itemToUpdate, { status: "incomplete" });
      } else {
        transaction.update(itemToUpdate, { status: "complete" });
      }
    });
  };

...
<input
  className="hidden"
  type="checkbox"
  id={"task_" + id}
  checked={item.status === "complete"}
  onChange={() => updateItem(item.id)}
   />
...

With this code, you can retrieve the document that needs updating from the "items" collection in Cloud Firebase using its id. Then, you use a transaction to update the item. If the current status is "complete," you change it to "incomplete," and vice versa. You can integrate this functionality into your task list by associating it with the respective checkboxes, allowing you to easily update the status of each task.

With these steps, your application should now retrieve and modify task statuses from Firebase.

Deployment

To deploy your application on Firebase Hosting, follow these steps:

1. Install Firebase Tools

Run the following command in your terminal to install Firebase Tools globally:

npm i -g firebase-tools

2. Log in to Firebase

Execute this command:

firebase login

An authentication window will appear. Log in using your Google credentials to continue.

3. Enable experimental web frameworks (if necessary)

If required, enable experimental web frameworks by entering:

firebase experiments:enable webframeworks

Now, you are ready to proceed with deploying your application to Firebase Hosting.

4. Initialize your project

Execute in your terminal:

firebase init hosting

Select a Firebase project to connect to your local project directory. Once you set up the hosting in your project, you will notice the addition of new files:

.firebaserc

{
  "projects": {
    "default": "my-nextjs-app-cb61e"
  }
}

firebase.json

{
  "hosting": {
    "source": ".",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "frameworksBackend": {
      "region": "us-east1"
    }
  }
}

These files configure your Firebase project and define hosting parameters.

5. Launch your to-do list app!

It's time to share your hard work with the world. In your terminal, run this command:

firebase deploy

Watch as your to-do list application comes to life on Firebase Hosting! Now you can access your app through a URL similar to this one:

https://my-nextjs-app-cb61e.web.app/

You now have your own fully deployed to-do list application. Great job! 🎉🚀

To sum up

Combining technologies like Next.js, Firebase, and Tailwind CSS unlocks a world of possibilities, including:

  • Rapid development: Next.js simplifies the creation of dynamic web applications with its built-in server-side rendering and efficient routing.
  • Real-time data: Firebase provides a real-time database, enabling instant updates and seamless synchronization across users and devices.
  • Stunning UI: Tailwind CSS follows a utility-first approach, making it a breeze to craft visually appealing and responsive interfaces.
  • Scalability: Thanks to the synergy between Next.js and Firebase, your application can effortlessly scale as your user base grows.

By mastering these technologies, you'll be well-prepared to craft web applications that blend functionality and aesthetics seamlessly. The possibilities are boundless—happy coding! 

------------------------------------

If you are ready to tackle new challenges and take your software development experience to the next level, explore our exciting job opportunities. We have amazing projects waiting for you!

Frequently Asked Questions