Getting Started With Next, Firebase & Tailwind
Tags: nextjs, firebase, tailwindcss, typescript
Published: 2022-10-05 15:05:56


Today we are going to be walking through setting up a [NextJS](https://nextjs.org/docs) project with protected routes using [Firebase Authentication](https://firebase.google.com/docs/auth) and [TailwindCSS](https://tailwindcss.com/) for styling.

The full code for this can be found in this [repository](https://github.com/damiensedgwick/next-firebase-tailwind) where you can clone the repository down or use some of the code as you please.

Lets start by creating a new project using Next and Typescript by running the following command:

`npx create-next-app@latest --ts`

Next we are going to install all of the dependencies for the project:

`npm i firebase`
`npm i -D tailwindcss postcss autoprefixer`

Now that we have installed the dependencies, we want to delete all of the default scaffolding that comes with the project.

All we want to be left with is `src/pages/_app.tsx`, `src/pages/index.tsx` and `src/styles/index.css` which we have placed within a `src` directory.

We also want to strip the previous code out of `index.tsx` so that it no longer relies on any of the resources we have deleted. It should look like this:

```tsx
import type { NextPage } from 'next'

const Home: NextPage = () => {
  return (
      <div>
        <h1>Hello, World!</h1>
      </div>
  )
}

export default Home
```

Time to setup Firebase and Tailwind.

## Firebase
For Firebase, we want to navigate to the [Firebase Console](https://console.firebase.google.com/) where we will want to click on `add project`.

This will walk you through creating a new project.

Once this has been done, we want to enable registration / logging in with a email and password.

Navigate to the project home by clicking the little home icon on the left hand side.

You should be presented with a page that has the following at the top:

![Preview of Firebase Console project home](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/utbz3pi9qfit3c0eysqv.png)

Click `authentication`, then navigate to `Sign-in Method` and select `Email and Password`, toggle this provider on.

Great! Our users will now be able to register and login to our application.

The last step for configuring Firebase at this point is to click the little settings cog icon and navigate to your project settings.

From here, if we scroll down, we will come across some code that looks like the following:

```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

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "<application-secret>",
  authDomain: "<application-secret>",
  projectId: "<application-secret>",
  storageBucket: "<application-secret>",
  messagingSenderId: "<application-secret>",
  appId: "<application-secret>"
};

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

Now we want to change this so that it takes advantage of using our `.env.local` file.

Replace the above example with the following and paste it in to a new file `src/utils/firebase.ts`

```js
import { initializeApp } from "firebase/app";
import { getAuth } from "@firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);
```

Then in our `.env.local` file, paste the following code:

```bash
NEXT_PUBLIC_FIREBASE_API_KEY="your-secret"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-secret"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="your-secret"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="your-secret"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="your-secret"
NEXT_PUBLIC_FIREBASE_APP_ID="your-secret"
```

That is all we need to do at the moment for setting up Firebase!

## Tailwind
For Tailwind, we want to run the following command:

`npx tailwindcss init -p`

This will create a couple of files for us, the only one we need to worry about at the moment is `tailwind.config.js`

Navigate to this file and paste the following code in:

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx}",
    "./src/components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Great! Now we just want to add the `@tailwind` directives to our global css file (index.css) so it should look like the following:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

html, body, #__next {
    height: 100%;
}
```

We should now have access to tailwind throughout our project which will enable us to style our application quickly, and easily.

Now that we have everything (more or less) setup, we are going to talk about the flow behind protected routes and how we are going to securing them.

NextJS uses file based routing. This means that by default, all pages created, will be publicly visible.

We want to prevent this in our application and we are going to do this by creating a new component called `ProtectedRoute.tsx` and that will look like this:

```tsx
import React, { ReactNode, useEffect } from "react";
import { useRouter } from "next/router";
import { useAuth } from "context/AuthContext";

interface ProtectedRouteProps {
  children: ReactNode;
}

const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
  const { user } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!user) {
      router.push("/login");
    }
  }, [router, user]);

  return <>{user ? children : null}</>;
};

export default ProtectedRoute;
```

We are then going to take this component and use it within our `_app.tsx` like so:

```tsx
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import "styles/globals.css";
import { AuthProvider } from "context/AuthContext";
import ProtectedRoute from "components/ProtectedRoute";

const protectedRoutes = ["/dashboard"];

function MyApp({ Component, pageProps }: AppProps) {
  const router = useRouter();

  return (
    <AuthProvider>
      {protectedRoutes.includes(router.pathname) ? (
        <ProtectedRoute>
          <Component {...pageProps} />
        </ProtectedRoute>
      ) : (
        <Component {...pageProps} />
      )}
    </AuthProvider>
  );
}

export default MyApp;
```

So what we have done here is created an array of route paths that should be protected, then checked to see if the path being navigated to is included in that array.

If it is included, we render the default `<Component {...pageProps} />` provided by NextJS within our `ProtectedRoute />` component which will then check for a user.

If there is an active session for this user within Firebase, they will be allowed to navigate to the protected route.

Otherwise they will be redirected to the login page where they can login or register if they do not have an account.

So now that we have a mechanism in place for protecting our routes, we want to enable it and you might have noticed this line within our `<ProtectedRoute />` component: `const { user } = useAuth()` - Lets explore that further.

We need a way to manage user sessions, or at least track them because Firebase will actually manage them in some length for us.

We are going to create a new file called `src/context/AuthContext.tsx` and paste the following code in:

```tsx
import {
  createContext,
  ReactNode,
  useContext,
  useState,
  useEffect,
} from "react";
import {
  createUserWithEmailAndPassword,
  onAuthStateChanged,
  signInWithEmailAndPassword,
  signOut,
  UserCredential,
} from "@firebase/auth";
import { auth } from "utils/firebase";

const AuthContext = createContext<{
  user: {
    uid: string;
    email: string | null;
    displayName: string | null;
  } | null;
  loading: boolean;
  login: (email: string, password: string) => Promise<UserCredential>;
  logout: () => Promise<void>;
  register: (email: string, password: string) => Promise<UserCredential>;
}>({
  user: null,
  loading: true,
  login: (email: string, password: string) =>
    signInWithEmailAndPassword(auth, email, password),
  logout: () => Promise.resolve(),
  register: (email, password) =>
    createUserWithEmailAndPassword(auth, email, password),
});

export const AuthProvider = ({ children }: { children: ReactNode }) => {
  const [loading, setLoading] = useState<boolean>(true);
  const [user, setUser] = useState<{
    uid: string;
    email: string | null;
    displayName: string | null;
  } | null>(null);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        setUser({
          uid: user.uid,
          email: user.email,
          displayName: user.displayName,
        });
      } else {
        setUser(null);
      }

      setLoading(false);
    });

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

  const login = (email: string, password: string) => {
    return signInWithEmailAndPassword(auth, email, password);
  };

  const logout = async () => {
    setUser(null);

    return await signOut(auth);
  };

  const register = (email: string, password: string) => {
    return createUserWithEmailAndPassword(auth, email, password);
  };

  return (
    <AuthContext.Provider value={{ loading, user, login, logout, register }}>
      {loading ? null : children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);

  if (context === undefined) {
    throw new Error("useCount must be used within a CountProvider");
  }

  return context;
};
```

There is quite a lot of code to digest in that snippet but I will do my best to summarise what is happening above.

* We create a new context object using `createContext` and we declare the shape of our context data within the kissing crocodiles `<>` followed by an object for default values.
* We then create an `AuthProvider` which will allow us to track state and we create a few functions that can be passed into our context so that we can use them elsewhere.
* Finally wrap our context in a custom hook called `useAuth` and export it.

There is an important bit to point out in the above snippet however and that is the code within our `useEffect`.

This is important because this is what we will use to track whether there is an active Firebase user or not.

Believe it or not, that is pretty much all you need to setup Firebase auth and protected routes within a NextJS application.

## Building out the rest of the application
As I mentioned earlier, you can find all of the code in this [repository](https://github.com/damiensedgwick/next-firebase-tailwind) but we still have some work to do yet before we can fully test our new application.

Firstly, we want to create a `<Layout />` and `<Header />` component so that we can share some styles and navigation links throughout the application.

I will post the code for those below:

```tsx
// src/layout/index.tsx
import { ReactNode } from "react";
import { Header } from "components/Header";

interface LayoutProps {
  children: ReactNode;
}

export const Layout = ({ children }: LayoutProps) => {
  return (
    <div>
      <Header />
      {children}
    </div>
  );
};
```

and

```tsx
// src/components/Header.tsx
import Link from "next/link";
import { useAuth } from "context/AuthContext";
import { useRouter } from "next/router";

export const Header = () => {
  const { user, logout } = useAuth();
  const router = useRouter();

  return (
    <div className="py-4  border-b-2 border-gray-900">
      <header className="container mx-auto flex flex-row items-center justify-between">
        <h1 className="px-4">
          <Link href="/">
            <a>NFT</a>
          </Link>
        </h1>

        <ul className="flex flex-row items-center justify-between">
          <li className="px-4">
            <Link href="/">
              <a>Home</a>
            </Link>
          </li>

          <li className="px-4">
            <Link href="/dashboard">
              <a>Dashboard</a>
            </Link>
          </li>
          <li className="px-4 border-l-2 border-gray-900">
            {user ? (
              <button
                type="button"
                onClick={async () => {
                  try {
                    await logout();
                    await router.push("/login");
                  } catch (error) {
                    console.log(error);
                  }
                }}
              >
                Logout
              </button>
            ) : (
              <Link href="/login">
                <a>Login</a>
              </Link>
            )}
          </li>
        </ul>
      </header>
    </div>
  );
};
```

Beautiful! That will make navigation easier and the header styling consistent.

The last thing we need to do is create all of our pages for which we can use the following commands:

`cd src && mkdir dashboard login register`

and whilst still in our src directory:

`touch dashboard/index.tsx login/index.tsx register/index.tsx`

The code for each page is below:

```tsx
// src/dashboard/index.tsx
import type { NextPage } from "next";
import { Layout } from "layout";

const Dashboard: NextPage = () => {
  return (
    <Layout>
      <main className="p-4">
        <div className="container mx-auto">
          <h1 className="text-2xl text-center">Your Dashboard</h1>
          <p className="mt-4 text-lg text-center">
            ⚠️⚠️ This should be a protected route and only accessible when there
            is a user ⚠️⚠️
          </p>
        </div>
      </main>
    </Layout>
  );
};

export default Dashboard;
```


```tsx
// src/login/index.tsx
import { useState } from "react";
import type { NextPage } from "next";
import Link from "next/link";
import { Layout } from "layout";
import { useRouter } from "next/router";
import { useAuth } from "../../context/AuthContext";

const Login: NextPage = () => {
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  const { login } = useAuth();
  const router = useRouter();

  return (
    <Layout>
      <main className="p-4">
        <div className="container mx-auto">
          <h1 className="text-2xl text-center">Login To Your Account</h1>
          <form
            className="w-3/4 max-w-md mt-4 mx-auto"
            onSubmit={async (e) => {
              e.preventDefault();

              try {
                await login(email, password);
                await router.push("/dashboard");
              } catch (error) {
                console.log(error);
              }
            }}
          >
            <div className="mb-4">
              <label htmlFor="email" />
              <input
                className="w-full p-2 border-2 border-gray-900"
                name="email"
                type="text"
                placeholder="Email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
              />
            </div>
            <div className="mb-4">
              <label htmlFor="password" />
              <input
                className="w-full p-2 border-2 border-gray-900"
                name="password"
                type="password"
                placeholder="Password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
            </div>
            <div className="mb-4">
              <button
                className="w-full py-2 px-6 text-gray-50 bg-gray-900"
                type="submit"
              >
                Login
              </button>
            </div>
          </form>

          <p className="text-center">
            Need an account?{" "}
            <Link href="/register">
              <a>Register</a>
            </Link>
          </p>
        </div>
      </main>
    </Layout>
  );
};

export default Login;
```


```tsx
// src/register/index.tsx
import { useState } from "react";
import type { NextPage } from "next";
import { useRouter } from "next/router";
import { Layout } from "layout";
import { useAuth } from "context/AuthContext";

const Register: NextPage = () => {
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  const { register } = useAuth();
  const router = useRouter();

  return (
    <Layout>
      <main className="p-4">
        <div className="container mx-auto">
          <h1 className="text-2xl text-center">Register Your Account</h1>
          <form
            className="w-3/4 max-w-md mt-4 mx-auto"
            onSubmit={async (e) => {
              e.preventDefault();

              try {
                await register(email, password);
                await router.push("/dashboard");
              } catch (error) {
                console.log(error);
              }
            }}
          >
            <div className="mb-4">
              <label htmlFor="email" />
              <input
                className="w-full p-2 border-2 border-gray-900"
                name="email"
                type="text"
                placeholder="Email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
              />
            </div>
            <div className="mb-4">
              <label htmlFor="password" />
              <input
                className="w-full p-2 border-2 border-gray-900"
                name="password"
                type="password"
                placeholder="Password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
              />
            </div>

            <div className="mb-4">
              <button
                className="w-full py-2 px-6 text-gray-50 bg-gray-900"
                type="submit"
              >
                Register
              </button>
            </div>
          </form>
        </div>
      </main>
    </Layout>
  );
};

export default Register;
```

Excellent! That should be everything we need!

If we quickly comeback to those additional functions we wrote, we can see that we are now using them in each of the forms to register or login and within the header to logout.

Now, if we have done everything correctly (and I haven't missed anything) we should now be able to run `npm run dev` to start our local dev server.

Navigating to `dashboard` by clicking on the link should redirect you to the login page.

As you do not yet have an account, click the link to register and fill in your details.

Upon successful creation, the application should now login and redirect you to the dashboard.

That is everything! I hope you've found this article helpful!

You can refresh, close the tab and open it again and your session will still be active!