· Joseph · DevOps  · 6 min read

Using Firebase and Firestore with NextJS and Docker - Part 2 - Setup firebase in Next.js

On our previous post shows how to integrate firebase with docker. This article we will integrate Next.js with firebase, and then deploy app with Github actions.  In this webapp, we can input cryptocurrency name, and upload logo, and therefore we use firestore to save Tokens collection and Cloud Storage to save logo images.

On our previous post shows how to integrate firebase with docker. This article we will integrate Next.js with firebase, and then deploy app with Github actions. In this webapp, we can input cryptocurrency name, and upload logo, and therefore we use firestore to save Tokens collection and Cloud Storage to save logo images.

Project structure

project structure We use NextJS 13 with appDir folder, so we have layout.tsx and page.tsx in appDir. Keep your eye on EmotionRootStyleRegistry.tsx, in this example we use Material UI and Emotion so we need to wrap a Provider (client component) in our layout.tsx. Only by doing this, we can use them with NextJS 13.

ref: https://github.com/emotion-js/emotion/issues/2928

Firebase config

We configure our Firebase object in configs/firebase/index.ts

configs/firebase/index.ts

import { initializeApp } from "firebase/app";
import { getStorage, connectStorageEmulator } from "@firebase/storage";
import {
  connectFirestoreEmulator,
  enableIndexedDbPersistence,
  initializeFirestore,
  CACHE_SIZE_UNLIMITED,
} from "@firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: "fir-firebase-94fdd.firebaseapp.com",
  projectId: "fir-firebase-94fdd",
  storageBucket: "fir-firebase-94fdd.appspot.com",
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
const cloudStorage = getStorage(app);
// const db = getFirestore(app);
const db = initializeFirestore(app, {
  cacheSizeBytes: CACHE_SIZE_UNLIMITED,
  ignoreUndefinedProperties: true,
});
if (process.env.NODE_ENV !== "production") {
  connectStorageEmulator(cloudStorage, "localhost", 9199);
  connectFirestoreEmulator(db, "localhost", 8080);
}
if (typeof window !== "undefined") {
  enableIndexedDbPersistence(db);
}
export { cloudStorage, db };

Firstly, FIREBASE_API_KEY, FIREBASE_MESSAGING_SENDER_ID, and FIREBASE_APP_ID are set in environment variable. Secondly, we connect our cloudStorage and db with emulator when we are not in production. Thirdly, we set db as a offline-first database by enableIndexedDbPersistence. And lastly we export cloudStorage and db directly.

appDir files

Now, let’s dig into our appDir files: layout.tsx, page.tsx, and HomePage.tsx.

app/layout.tsx

import { ReactNode } from "react";
import Copyright from "@/components/Copyright";
import EmotionRootStyleRegistry from "./EmotionRootStyleRegistry";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html>
      <head></head>
      <body>
        <EmotionRootStyleRegistry>
          {children}
          <Copyright />
        </EmotionRootStyleRegistry>
      </body>
    </html>
  );
}
export const metadata = {
  title: {
    default: "NextJS and Firebase",
    template: "%s | NextJS and Firebase",
  },
  description: "Generated by create next app",
  viewport: {
    width: "device-width",
    initialScale: 1,
    maximumScale: 1,
  },
};

The metadata in layout.tsx will render into <head></head>. Besides RootLayout, you can have many nested layout in each sub-routes, and use generateMetadata with title template to generate title in each nested layout.

app/page.tsx

import HomePage from "./HomePage";

export default function Page() {
  return <HomePage />;
}

Our page.tsx is a very simple server component. We can fetch data or set metadata over here if necessary.

app/HomePage.tsx

"use client";

import { Container, Typography, Box, Button } from "@mui/material";
import { useCallback, useEffect, useState } from "react";
import { useShareTokenDialog } from "@/hooks/useShareTokenDialog";
import TokenInfoDialog from "@/components/dialogs/TokenInfoDialog";
import * as TokenService from "@/services/TokenService";
import { TokenInfoProps } from "@/types/TokenProps";
import Token from "@/components/Token";

export default function HomePage() {
  const { setDialog } = useShareTokenDialog();
  const [tokens, setTokens] = useState<TokenInfoProps[]>([]);
  const fetchTokens = useCallback(async () => {
    const data = await TokenService.getTokens();
    setTokens(data);
  }, []);

  useEffect(() => {
    fetchTokens();
  }, [fetchTokens]);

  return (
    <Container maxWidth="lg">
      <Box
        sx={{
          my: 2,
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
        }}
      >
        <Typography variant="h4" component="h1">
          Token List
        </Typography>
        <Button
          variant="contained"
          onClick={() => setDialog({ open: true, isCreate: true })}
        >
          Add Token
        </Button>
      </Box>
      <Box display="flex" sx={{ mx: "auto", width: "90%" }}>
        {tokens.map((token) => (
          <Token key={token.id} {...token} />
        ))}
      </Box>
      <TokenInfoDialog onSave={fetchTokens} />
    </Container>
  );
}

Our HomePage.tsx client component is more complicted. Let’s us focus on useShareTokenDialog hook and TokenService. Instead of redux, in this example we use use-between to share state between components. It’s really easy and useful.

hooks and Service

We won’t go through every component, but you can find it on our github repo. But how we save data to firesotre and storage? We implement it in our TokenService and UploadService!

services/TokenService.ts

import { v4 as uuidv4 } from "uuid";
import { collection, getDocs, getDoc, doc } from "firebase/firestore";
import { TokenInfoProps } from "@/types/TokenProps";
import setDocWithTimeoutError from "@/libs/setDocWithTimeoutError";
import { db } from "@/configs/firebase";

export const setToken = async (
  tokenInfo: TokenInfoProps
): Promise<TokenInfoProps> => {
  if (!tokenInfo.id) {
    tokenInfo.id = uuidv4();
  }
  try {
    await setDocWithTimeoutError(`tokens/${tokenInfo.id}`, tokenInfo);
  } catch (e) {}
  return tokenInfo;
};

export const getToken = async (id: string): Promise<TokenInfoProps | null> => {
  const docRef = doc(db, "tokens", id);
  const tokenSnapshot = await getDoc(docRef);
  if (tokenSnapshot.exists()) {
    return tokenSnapshot.data() as TokenInfoProps;
  } else {
    return null;
  }
};
export const getTokens = async (): Promise<TokenInfoProps[] | []> => {
  const tokensSnapshot = await getDocs(collection(db, "tokens"));
  return tokensSnapshot.docs.map((snap) => snap.data() as TokenInfoProps);
};

services/UploadService.ts

import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
import { cloudStorage } from "configs/firebase";

export const uploadFromBlobAsync = async (
  file: File,
  folder: string,
  onSuccess: (url: string) => void,
  onError = (err?: Error) => {}
) => {
  if (!file) return null;
  const storageRef = ref(cloudStorage, `/${folder}/${Date.now()}_${file.name}`);
  const uploadTask = uploadBytesResumable(storageRef, file);
  uploadTask.on(
    "state_changed",
    () => {},
    onError,
    () => {
      getDownloadURL(uploadTask.snapshot.ref).then((url) => {
        onSuccess(url);
      });
    }
  );
};

As you see, there are many Firestore methods such as collection, getDocs, getDoc, and doc, and we import db from configs so that we can manipulate firestore database. On the other hand, our UploadService imports cloudStorage and handles Storage methods.

You can implement your components as you wish, but now we’re gonna to deploy to Firebase.

Deploy

We use Github action to deploy our app. You can generate a template by firebase init hosting, choose webframeworks, and login authorize Github to deploy.

.github/workflows/firebase-hosting-merge.yml

name: Deploy to Firebase Hosting on merge

permissions:
  checks: write
  contents: read
  pull-requests: write

'on':
  push:
    branches:
      - main

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./app
    environment: production
    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - run: yarn install --immutable && yarn build

      - name: Enable Firebase Experiments
        run: /usr/local/bin/npx firebase-tools@latest experiments:enable webframeworks

      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          entrypoint: "./app"
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_FIR_FIREBASE_94FDD }}'
          channelId: live
          projectId: fir-firebase-94fdd
        env:
          FIREBASE_CLI_EXPERIMENTS: webframeworks
          FIREBASE_API_KEY: '${{ secrets.FIREBASE_API_KEY }}'
          FIREBASE_MESSAGING_SENDER_ID: '${{ secrets.FIREBASE_MESSAGING_SENDER_ID }}'
          FIREBASE_APP_ID: '${{ secrets.FIREBASE_APP_ID }}'

By the way, we set production environment and environment variables on Github repository settings. Github environment

It’s time to commit, push, deploy, and go to your firebase url to see result.

If you got an deploy 403 error, please add some IAM permissions by checking:

  1. https://stackoverflow.com/questions/69105552/http-error-403-permission-denied-to-enable-service-cloudfunctions-googleapis
  2. https://github.com/FirebaseExtended/action-hosting-deploy/issues/253

Conclusion

Finally we get an opportunity to use NextJS 13 appDir. Although it’s still an experimental feature, we feel that is more intuitive to organize server-side and client-side components. Some packages are not supported now, but you still can find a patch easily by Google or ChatGPT.

In this simple example, even through we don’t build relationships in database and don’t use Firebase Auth or Firebase Function, we use NextJS 13 appDir folder integrating with Firebase, Firestore, and Cloud Storage. And, it is also worth of reference if you want to use Firebase Emulator in docker-compose. If you need more details about this project, please go to Github repo. Hope this is useful for you.

Back to Blog

Related Posts

View All Posts »
Using Firebase and Firestore with NextJS and Docker - Part 1 - Setup firebase in docker

Using Firebase and Firestore with NextJS and Docker - Part 1 - Setup firebase in docker

Last year, I got a case to use firebase and firestore with Next.js. I've been fullstack many years, so I haven't tried to use firebase and firestore. There was a great chance to give it a try. In this article I'll show how to use firebase and firestore in Docker and Next.js. If you don't have backend support, or you don't want to build whole backend, database, and infrastructure, you would probably think this is a useful way.

Install Docker / Docker-compose on CentOS 8

新的一篇文章來講講最近踩的雷,起因手邊有個案子開了一台CentOS 8機器給我,讓我在上面設定docker跟跑起服務。實在跟CentOS很不熟的我決定寫篇備忘錄。 一開始就先開台乾淨的CentOS VM來準備被我玩爛... install centos Install Docker 然後,讓我慢慢安裝docker。

Switching from vim to Neovim!

Switching from vim to Neovim!

I've been using Vim for a long time and finally switched to Neovim. The initial thought of switching came after the author of VIM passed away in August 2023, as I didn’t have the time to try other editors. After a year, “vibe coding” grew up, so I started thinking how to integrate AI into my editor and surveying how to use AI in Vim, which led to this journey. TOC Main differences Tab vs. Buffer: ref: https://www.reddit.com/r/neovim/comments/13iy0p0/why_some_people_use_buffers_for_tabs_and_not_vim/ In vim, I used vsp or tabnew to open or edit a file and mapped tabprevious command with Tab key to navigate between tabs. However, in Neovim, I used buffer instead of tabs and mapped BufferLineCyclePrev with Shift + h for switching buffers. coc vs. native lsp: I configured many coc settings to support TypeScript and JavaScript language servers, including linting and Prettier on save, go-to definition or reference, and codelens. After using Neovim, I converted all of these settings to nvim-lspconfig and mason, among others. Lua supports: Although I’m not familiar with Lua, it allows me to write more readable configuration files using modules, functions, and tables (objects) for Neovim. AI supports: Vibe coding! I found many plugins to integrate LLMs and finally selected three to use: avante.nvim Let your Neovim be a Cursor AI IDE – it's undergoing rapid iterations and many exciting features will be added successively; it updates almost every day! CodeCompanion CodeCompanion can customize workflows and prompts, and its Action Palette is a really useful tool. gp.nvim GP means GPT Prompt, and it's an Neovim AI plugin. It helps me write prompts through Neovim's cmdline. I usually switch between these plugins, and I'm still thinking about my 'vibe-way' to use them. Because of supporting Ollama LLM, all of them can be used offline. For now, I've used Neovim for three months, and got to know Neovim. In my experience, Neovim is really more productive than vim. Reference AI in Neovim How to set up Neovim for coding React ZazenCodes Neovim Rewrite by Avante + Ollama gemma3:4b ------ For a long time, I relied on Vim for my coding. However, after Bram Moolenaar’s passing in August 2023 – a significant influence in the Vim community – I decided it was time for a change. I began exploring alternative editors, ultimately settling on Neovim. This journey wasn’t just about switching editors; it was about integrating AI into my workflow and redefining my coding experience. Key Differences: Tabs vs. Buffers One of the first things I noticed was the shift from tabs to buffers in Neovim. In Vim, I frequently used vsp or tabnew to open or edit files, navigating between tabs with the Tab key and Tabprevious command. Neovim, however, utilizes buffers, offering a more streamlined approach. I configured BufferLineCyclePrev with Shift + h for seamless buffer switching, alongside nvim-tree/nvim-web-devicons and akinsho/bufferline.nvim. Leveraging Language Servers with coc and nvim-lspconfig I configured many coc settings to support TypeScript and JavaScript language servers, including linting and Prettier on save, go-to definition or reference, and codelens. Recognizing the power of language servers, I then converted all of these settings to nvim-lspconfig and mason.nvim, streamlining my development environment. Lua Configuration for Readability Although I’m relatively new to Lua, it allows me to write more readable configuration files using modules, functions, and tables (objects) for Neovim. Here’s a snippet of my configuration: AI-Powered Productivity with avante.nvim, CodeCompanion, and gp.nvim To truly elevate my coding experience, I integrated several AI plugins. I selected: avante.nvim**: This plugin transforms Neovim into a Cursor AI IDE, undergoing rapid iterations and adding exciting new features daily. CodeCompanion**: This plugin allows for customizable workflows and prompts, with a particularly useful Action Palette. gp.nvim**: (GPT Prompt) – This plugin helps me write prompts through Neovim’s command line interface, leveraging folke/noice.nvim. Because of supporting Ollama LLM, all of these plugins can be used offline. I’m still experimenting with how to best utilize these plugins – a “vibe-way” to coding! Resources for Further Exploration AI in Neovim How to set up Neovim for coding React ZazenCodes Neovim

AWS Cloud Development Kit (CDK) project structure

AWS Cloud Development Kit (CDK) project structure

Previously blog I used NodeJs/Typescript as a backend and deployed with AWS Cloud Development Kit (AWS CDK). The same framework, but more complex than the sample, is used on our Firstage. So this post I will show how we structure our AWS CDK project codebase. Project Structure project structure