Logo
Empezar a leer

© 2025 - alckordev

Integrar Firebase Realtime with Next.js (Page Router)

19 marzo, 2023

6 minutos de lectura

0

Integrar Firebase Realtime with Next.js (Page Router)

En este artículo aprenderemos a integrar Firebase Realtime Database en una aplicación Next.js para construir un sistema de comentarios en tiempo real, que nos permita almacenar y visualizar los comentarios.

Desde la consola de Firebase crearemos un nuevo proyecto, agregaremos una aplicación web y crearemos una base de datos realtime.

Para poder leer y escribir en nuestra base de datos debemos definir unas reglas desde la consola de Firebase, para este ejemplo, definiremos la arquitectura de la siguiente manera: cada comentario pertenecerá a un hilo, y cada hilo será una representación de un post en nuestra aplicación, asumiendo que los posts se obtiene desde otra fuente.

Las reglas deberían quedar de la siguiente manera:

{
  "rules": {
    "threads": {
      ".read": true,
      ".write": true,
      ".indexOn": ["identifier"]
    },
    "comments": {
      ".read": true,
      ".write": true,
      ".indexOn": ["thread"]
    }
  }
}

Configurar el SDK de Firebase

Instalaremos el paquete de Firebase en nuestra aplicación

npm i firebase

Es probable que ya hayas configurado el SDK de Firebase alguna vez. Creamos un archivo lib/firebase.ts, el cual debería verse algo como esto:

// lib/firebase.ts

import { initializeApp } from "firebase/app";
import { getDatabase } from "firebase/database";

const config = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DB_URL,
  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,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

const app = initializeApp(config);
const database = getDatabase(app);

export { database };

Almacenamiento y obtención de un hilo

Creamos un archivo lib/firebase-utils.ts y agregaremos algunas funciones como estas:

// lib/firebase-utils.ts

import * as fbdb from "firebase/database";
import { database } from "./firebase";

export function getWithKey(data: any, options?: any) {
  const array = Object.keys(data).map((key) => {
    return {
      ...data[key],
      key,
    };
  });

  return options?.isFirstOrDefault ? array[0] : array;
}

export async function setThread(identifier: string) {
  const threadRef = fbdb.push(fbdb.ref(database, "threads"));

  await fbdb.set(threadRef, { identifier: identifier });

  const snapshot = await fbdb.get(threadRef);

  const thread = snapshot.val();

  return { ...thread, key: snapshot.key };
}

export async function getThread(identifier: string) {
  const threadRef = fbdb.ref(database, "threads");

  const endpoint = fbdb.query(
    threadRef,
    fbdb.orderByChild("identifier"),
    fbdb.equalTo(identifier)
  );

  const snapshot = await fbdb.get(endpoint);

  if (snapshot.exists()) {
    return getWithKey(snapshot.val(), { isFirstOrDefault: true });
  }

  return setThread(identifier);
}

Nos dirigimos al archivo pages/posts/[id].tsx, el cual será responsable de almacenar y obtener el hilo representativo del post.

Cabe mencionar que para la interfaz del proyecto estaré usando Chakra UI. Pueden revisar su documentación oficial si es necesario.

// pages/posts/[id].tsx

import { Container, Heading, Box, Divider } from "@chakra-ui/react";
import { getThread } from "@/lib/firebase-utils";

export default function Post({ post, thread }: any) {
  console.log(thread);
  return (
    <Container>
      <Heading my={8}>{post.title}</Heading>
      <Box>{post.body}</Box>
    </Container>
  );
}

export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await res.json();

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post: any) => ({
    params: { id: post.id.toString() },
  }));

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false };
}

export async function getStaticProps({ params }: any) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 1
  const res = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`
  );
  const post = await res.json();

  // Get thread
  const thread = await getThread(params.id);

  return {
    props: { post, thread },
  };
}

Como podemos observar en la función getStaticProps, estamos obteniendo el hilo almacenado en la base de datos como un objeto que se encuentra disponible en los props.

Almacenamiento y obtención de comentarios

Nos dirigimos al archivo lib/firebase-utils.ts y agregamos la siguiente función:

// lib/firebase-utils.ts

export async function getCommentsByThread(identifier: string) {
  const commentRef = fbdb.ref(database, "comments");

  const endpoint = fbdb.query(
    commentRef,
    fbdb.orderByChild("thread"),
    fbdb.equalTo(identifier)
  );

  const snapshot = await fbdb.get(endpoint);

  if (snapshot.exists()) {
    return getWithKey(snapshot.val());
  }

  return undefined;
}

Ahora vamos a crear los siguientes componentes: Editor, Comment y DiscussionThread.

Editor

// components/Editor.tsx

import { VStack, FormControl, Input, Textarea, Button } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { formatISO } from "date-fns";
import * as fbdb from "firebase/database";
import { database } from "@/lib/firebase";

export const Editor = ({
  thread,
  placeholder = "Join the conversation...",
}: any) => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: yupResolver(
      yup.object().shape({
        author_name: yup.string().required(),
        author_email: yup.string().email().required(),
        message: yup.string().min(2).required(),
      })
    ),
  });

  const toast = Chakra.useToast();

  const onSubmit = handleSubmit(async (values) => {
    try {
      const commentRef = fbdb.push(fbdb.ref(database, "comments"));

      await fbdb.set(commentRef, {
        thread,
        author: {
          name: values.author_name,
          email: values.author_email,
        },
        message: values.message,
        createdAt: formatISO(new Date()),
      });

      reset({ message: "" });
    } catch (err) {
      toast({
        description: "¡Oops! Something went wrong.",
        status: "error",
      });
    }
  });

  return (
    <VStack minW="100%" as="form" onSubmit={onSubmit}>
      <FormControl isInvalid={errors.author_name ? true : false}>
        <Input placeholder="Name" size="sm" {...register("author_name")} />
      </FormControl>
      <FormControl isInvalid={errors.author_email ? true : false}>
        <Input placeholder="E-mail" size="sm" {...register("author_email")} />
      </FormControl>
      <FormControl isInvalid={errors.message ? true : false}>
        <Textarea
          placeholder={placeholder}
          size="sm"
          resize="none"
          {...register("message")}
        />
      </FormControl>
      <Button type="submit" isLoading={isSubmitting}>
        Comment
      </Button>
    </VStack>
  );
};

Comment

// components/Comment.tsx

import { Box, VStack, Flex, Avatar, Heading, Text } from "@chakra-ui/react";
import { format, parseISO } from "date-fns";

export const Comment = ({ thread, comment, ...rest }: any) => {
  return (
    <Box minW="100%" {...rest}>
      <VStack align="flex-start">
        <Flex gap={4} align="center">
          <Avatar size="sm" name={comment.author.name} />
          <Box>
            <Heading size="sm">{comment.author.name}</Heading>
            <Text>{format(parseISO(comment.createdAt), "d MMMM, yyyy")}</Text>
          </Box>
        </Flex>
        <Box>{comment.message}</Box>
      </VStack>
    </Box>
  );
};

DiscussionThread

// components/DiscussionThread.tsx

import { useEffect, useState } from "react";
import { Box, Divider, VStack, StackDivider } from "@chakra-ui/react";
import { getCommentsByThread } from "@/lib/firebase-utils";
import * as fbdb from "firebase/database";
import { database } from "@/lib/firebase";
import { Editor } from "./Editor";
import { Comment } from "./Comment";

export const DiscussionThread = ({ identifier }: { identifier: string }) => {
  const [comments, setComments] = useState<any[]>([]);

  useEffect(() => {
    // Get all comments by thread
    async function loadComments() {
      const data = await getCommentsByThread(identifier);

      if (!data) return;

      setComments(data.reverse());
    }

    loadComments();

    // Watching new comments
    const commentRef = fbdb.ref(database, "comments");

    const endpoint = fbdb.query(
      commentRef,
      fbdb.orderByChild("thread"),
      fbdb.equalTo(identifier)
    );

    fbdb.onChildAdded(endpoint, (snapshot) => {
      const newComment = { ...snapshot.val(), key: snapshot.key };
      const existingComment = comments.find((c) => c.key === newComment.key);

      if (existingComment) return;

      setComments((prevComments) => [newComment, ...prevComments]);
    });

    return () => {
      // Stop watching when component is unmounted
      fbdb.off(endpoint, "child_added");
    };
  }, [identifier]);

  return (
    <Box>
      <Editor thread={identifier} />
      <Divider borderColor="gray.500" my={8} />
      <VStack divider={<StackDivider />} spacing={6}>
        {comments.map((comment) => (
          <Comment key={comment.key} thread={identifier} comment={comment} />
        ))}
      </VStack>
    </Box>
  );
};

Finalmente nos dirigimos al archivo pages/posts/[id].tsx y agregamos el siguiente código:

// pages/posts/[id].tsx

import { DiscussionThread } from "@/components/DiscussionThread";

export default function Post({ post, thread }: any) {
  return (
    <Container>
      ...
      <Divider borderColor="gray.500" my={8} />
      <DiscussionThread identifier={thread.key} />
    </Container>
  );
}

Resultado final

Si has llegado hasta aquí, deberías tener algo como esto en la siguiente ruta: http://localhost:3000/posts/1.

El código completo de este proyecto está disponible en Github.

¡Espero que te haya gustado y te haya sido útil!, Si tienes sugerencias o mejoras que hacer, eres libre de realizar un "pull request" para contribuir.

test...

¿Te gustó lo que leíste?

Si lo deseas, puedes apoyarme con una donación voluntaria. Tu aporte me permite dedicar más tiempo a investigar, escribir y mejorar la calidad del contenido que publico. ¡Muchísimas gracias por considerar impulsar este proyecto!

Cómprame un café

Compartir en: