Getting Started With Next, Firebase & Tailwind

Damien Sedgwick - Oct 5 '22 - - Dev Community

Today we are going to be walking through setting up a NextJS project with protected routes using Firebase Authentication and TailwindCSS for styling.

The full code for this can be found in this repository 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:



import type { NextPage } from 'next'

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

export default Home


Enter fullscreen mode Exit fullscreen mode

Time to setup Firebase and Tailwind.

Firebase

For Firebase, we want to navigate to the Firebase Console 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

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:



// 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);


Enter fullscreen mode Exit fullscreen mode

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



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);


Enter fullscreen mode Exit fullscreen mode

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



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"


Enter fullscreen mode Exit fullscreen mode

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:



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


Enter fullscreen mode Exit fullscreen mode

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



@tailwind base;
@tailwind components;
@tailwind utilities;

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


Enter fullscreen mode Exit fullscreen mode

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:



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;


Enter fullscreen mode Exit fullscreen mode

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



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;


Enter fullscreen mode Exit fullscreen mode

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:



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;
};


Enter fullscreen mode Exit fullscreen mode

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



// 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>
  );
};


Enter fullscreen mode Exit fullscreen mode

and



// 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>
  );
};


Enter fullscreen mode Exit fullscreen mode

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:



// 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;


Enter fullscreen mode Exit fullscreen mode


// 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;


Enter fullscreen mode Exit fullscreen mode


// 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;


Enter fullscreen mode Exit fullscreen mode

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!

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