Malena

Firebase y React para principiantes, parte 1

Creando una base de datos en Firebase y obteniendo la información en React

Si ya sabés los básicos de front end - HTML, CSS y JavaScript - y podés hacer una app con React que se comunique con APIs para desplegar información, un próximo paso en tu aprendizaje es lograr comunicación con una base de datos propia.

Firebase es una excelente alternativa ya que la configuración inicial es relativamente sencilla, permite escalar con facilidad si en algún momento nuestra app lo requiere, y podemos ir agregando distintas funcionalidades ofrecidas por el entorno.

La mayor desventaja si sos principiante es que Firebase se ocupa de tantas cosas que buena parte del tiempo vas a sentir que estás siguiendo instrucciones sin entender bien qué estás haciendo. Mi recomendación, si tu stack es de front y querés aprender más, es que inviertas tiempo en un curso de Node, para poder hacer tus propios servidores (y muchas cosas más!).

Manos a la obra

Vamos a comenzar con una app de React ya funcionando.

Si esta es tu primera vez trabajando con Firebase, incorporarlo en una app más compleja va a ser más difícil a la larga. Mi consejo es que comiences con la aplicación más sencilla posible - un solo componente, App.

Para empezar, crea una cuenta en la web de Firebase. Navegá a la consola de Firebase y hacé clic en "Add project". Elegí un nombre. No es necesario que agregues analytics. Una vez creado tu proyecto, vas a ser redirigido a la pagina principal desde la cual podés controlar tu proyecto y agregar funcionalidades. Además de una base de datos, Firebase te ofrece autenticación de usuarios, hosting, entre muchas otras cosas, así que vale la pena explorar todo lo que ofrece tras aprender los básicos.

El próximo paso es agregar Firebase a nuestra app. Firebase suele cambiar su sitio con más frecuencia de la que me gustaría, pero al momento de escribir este tutorial, vas a ver algo como esto. Elegí el botón "web".

alt

Vas a elegir un "nickname" para tu App. Este es exclusivo para Firebase, tus usuarios no lo verán. No es necesario agregar hosting. Una vez creado el nombre, vas a ver algo como esto:

  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "AIzaKHGbiiE5kjhgfVLeuPertyuiGi9Et789xeNHBFlhD8",
    authDomain: "tutorial-collected-notes-456789.firebaseapp.com",
    databaseURL: "https://tutorial-collected-notes-456789.firebaseio.com",
    projectId: "tutorial-collected-notes-456789",
    storageBucket: "tutorial-collected-notes-456789.appspot.com",
    messagingSenderId: "64456789016",
    appId: "1:646567896:web:5b456789f452fd7890-b0bb0d"
  };

(Obviamente, estos datos son falsos. Nunca deberías compartir tu apiKey con nadie).

Con esta configuración hecha, es momento de agregar firebase a tu app. En la terminal, dentro del directorio de tu proyecto, instalá firebase:

npm i firebase

Mientras se instala, crea un nuevo archivo firebase.js en tu carpeta /src o /components.

Para que puedas compartir este proyecto o subirlo a la web, es necesario que los datos como la Api Key estén protegidos. Para eso vamos a usar variables de entorno.

En el archivo que creaste, agregá este objeto.

const config = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
};

Luego crea un nuevo archivo .env en la carpeta raíz de tu proyecto. Este archivo debe ser agregado a .gitignore para que tus variables no estén expuestas al momento de subir tu proyecto a github o gitlab. En tu archivo .env escribí lo siguiente, reemplazando los valores por los que sacaste de tu configuración de firebase.

REACT_APP_API_KEY=AIzaKHGbiiE5kjhgfVLeuPertyuiGi9Et789xeNHBFlhD8
REACT_APP_AUTH_DOMAIN=https://tutorial-collected-notes-456789.firebaseio.com
REACT_APP_DATABASE_URL=tutorial-collected-notes-456789
REACT_APP_PROJECT_ID=tutorial-collected-notes-456789.appspot.com
REACT_APP_STORAGE_BUCKET=tutorial-collected-notes-456789.appspot.com
REACT_APP_MESSAGING_SENDER_ID=64456789016
REACT_APP_APP_ID=1:646567896:web:5b456789f452fd7890-b0bb0d

Recordá que los valores en los archivos .env no se escriben entre comillas.

Para este momento, firebase ya debe estar instalado, así que volvamos al archivo firebase.js e importemos firebase/app.

import * as firebase from 'firebase/app';

const config = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
};

firebase.initializeApp(config);

Este código se comunica con Firebase y le envía nuestra configuración. Ya estamos listos para comenzar a agregar información a una base de datos y poder accederla desde nuestro código.

Configurando nuestra base de datos

Volvamos a Firebase. En el menú de la izquierda vas a ver una sección "Database". Vamos a ella.

Firebase ofrece dos servicios para bases de datos: Firestore y Realtime Database. Te recomiendo elegir Firestore, ya que es la más reciente y -en mi experiencia- la más utilizada. Hagamos clic en "Create database"

Firebase nos va a pedir que configuremos nuestro proyecto en "Test Mode" o "Production Mode". En "test mode" la información de nuestra base de datos podrá ser leída y modificada por cualquier usuario. Obviamente esto no es lo más seguro, pero por ahora vamos a iniciarlo de esa manera. No deberías publicar tu aplicación en Test mode: mantenelo solo mientras estes trabajando de manera local, e incluso en ese caso no agregues información privada a tu base de datos. Tomá en cuenta que si en 30 días no modificas esta configuración, Firebase va a bloquear las llamadas a tu base de datos.

Luego vas a tener que agregar una ubicación para Cloud Firestore. Este paso no es importante en este punto, elegí el que se te ofrezca por defecto. Una vez creada vas a ver algo como esto:

alt

Aquí vas a poder agregar información a tu base de datos. En firestore guardamos nuestros datos en documentos, que a su vez se organizan en colecciones. La información en los documentos se guarda en sets de claves/valores. Si querés aprender más sobre el data model de Firestore, la documentación oficial es completa, clara y accesible.

Por ahora, hagamos clic en "Start Collection". Voy a hacer todos los ejemplos con una colección de libros, pero sentite libre de adaptarlo a cualquier proyecto que tengas ganas de hacer. Por ejemplo, podrías hacer una colección de productos en venta para una web de e-commerce, o cualquier otra cosa que te interese hacer.

Le agregamos un id a la colección nueva. Con este nombre la identificaremos tanto en Firestore como al llamarla desde nuestra app, así que debe ser sencillo y claro. "books" en mi caso será suficiente.

Una vez elegido el nombre, nos aparecerá esto:

alt

Cada documento dentro de una colección debe tener un ID único. Si no sabés cual poner, Firestore te ofrece un "auto ID". Yo voy a escribir IDs numericos para que quede más claro este tutorial, pero en general te recomiendo usar el auto-id de Firebase.

Luego podemos empezar a agregar campos en nuestro documento. Esto es más o menos como crear un objeto en Javascript: ponemos el nombre de la propiedad en "field" y su valor en "value". La diferencia es que aquí debemos especificar el tipo de dato que vamos a guardar. Los tipos que permite Firestore son strings, números, booleanos, arrays, entre otros. Vamos a agregar varios campos: en mi caso, el titulo del libro, su autor, el año de publicación, si está disponible para ser prestado o no y una lista de géneros. La idea es mostrarte los distintos tipos de información que podemos guardar aquí: vos usá aquellos que te sirvan y tengan sentido para la información que querés guardar.

alt

Notá que no pongo comillas en los strings, ni corchetes en el array: no es necesario, ya que el tipo de dato está especificado explicitamente. También tomá en cuenta que debo especificar el tipo de dato para cada elemento de un array, aunque no es necesario que sean todos el mismo. Te recomiendo, aunque esto ya es preferencia, escribir los nombres de los campos en camelCase, ya que queremos mantener coherencia con lo que usaremos en el código del front.

Tras guardar este objeto, creemos algunos más. Tomá en cuenta que Firestore no te obliga a que todos los documentos dentro de una colección tengan la misma estructura, pero es muy recomendable que la mantengas siempre: los mismos campos y los mismos tipos para cada documento. Si no tenés un dato en particular (por ejemplo un libro no tiene fecha de publicación) te recomiendo agregar el campo y darle valor "null". Esto va a evitarte errores en el front más adelante. Si olvidas alguno no hay problema, podés agregarlo después.

Una vez agregados algunos documentos más, vas a terminar con algo como esto. Podes revisar la información, editar los campos, etc. No te preocupes si no están en el orden que los escribiste: firestore los ordena alfabéticamente.

alt

Viendo la información en el front

Volvamos a nuestro componente firebase.js. Necesitamos hacer dos cosas: importar firestore, y crear una variable para el método que se comunica con él. A esa variable yo la llame "db", vos ponele cualquier nombre que te sirva. Pero no olvides exportarla.

import * as firebase from 'firebase/app';
import 'firebase/firestore';

const config = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID,
};

firebase.initializeApp(config);
export const db = firebase.firestore();

Si tenías tu proyecto corriendo, es probable que debas interrumpirlo y comenzar de nuevo para que las variables de entorno y la comunicación con firebase funcionen.

Vamos al componente principal, App.js, y escribamos cualquier información que podamos ver en la pantalla:

import React from 'react';

const App = () => {
  return (
    <div>
      <h1>Los libros de Malena</h1>
      <p>No hay libros aún.</p>
    </div>
  );
}

export default App;

Para comunicarnos con nuestra base de datos, debemos importar la variable que creamos desde el archivo donde esté, en mi caso, /components/firebase.js. Como no es un export default, debemos agregarle corchetes. Asi:

import { db } from './components/firebase';

Firestore nos permite llamar a todos los documentos dentro de una colección, a los documentos de una colección que cumplan ciertas condiciones, o a un solo documento en específico. Comencemos por llamar a todos los documentos.

Usaremos un efecto, como corresponde al comunicarnos con una API.

  useEffect(() => {
    db.collection('books')
      .get()
      .then(querySnapshot => {
        querySnapshot.forEach(doc => {
          console.log(doc.data());
        });
      });
  }, []);

El código intimida a primera vista, pero viendolo por partes es sencillo. Dentro del efecto nos estamos comunicando con nuestra base de datos (db), y dentro de ella, con una colección, identificandola con el nombre que le dimos en firestore ("books"). Obtenemos la información con el método get() que nos devuelve una "query snapshot": una manera en que Firestore optimiza el envío de información. Por cada query snapshot vamos a tener un documento. Si accedemos a la propiedad doc directamente vamos a tener info sobre el documento: si queremos la información que nosotros guardamos, debemos usar el método data().

Ejecutemos este código y vayamos a la consola. Ahí deberías tener un mensaje en la consola por cada uno de los documentos de tu colección.

alt

Ahora podemos guardar esta información para usarla en nuestra aplicación, por ejemplo, en un estado. Modifiquemos el código para crear un estado y guardar esta información una vez recibida:

const App = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    db.collection('books')
      .get()
      .then(querySnapshot => {
        const books = [];
        querySnapshot.forEach(doc => {
          books.push(doc.data());
        });
        setData([...books]);
      });
  }, []);

  return (

Con la información ya guardada en el estado, podemos modificar nuestro JSX para poder verla.

  return (
    <div>
      <h1>Los libros de Malena</h1>
      {data.length ? (
        data.map(book => (
          <article key={book.id}>
            <h2>{book.title}</h2>
            <h3>{book.author}</h3>
            <p>{book.year}</p>
          </article>
        ))
      ) : (
        <p>No hay libros aún.</p>
      )}
    </div>
  );

Para que esta sea una app terminada, debemos continuar agregando estilado, componentizando nuestro código y agregando más funcionalidades.

Pero antes vamos a modificar nuestro efecto para que, en lugar de obtener toda una colección, obtengamos solo los documentos que cumplen ciertas condiciones: en este caso, haber sido publicado después de 1900.

Volvamos a nuestro efecto y modifiquemos solo la primera linea para que se vea asi:

    db.collection('books').where("year", ">", 1900)

De esta manera, Firestore nos devuelve solo los documentos que cumplen la condición especificada. Hay muchas maneras de filtrar la información: la documentación oficial es el mejor lugar para hacerlo.

Con este tipo de funcionalidad podemos hacer un filtro para que el usuario obtenga solo los libros publicados en cierto año, solo los libros publicados por algunos autores en específico, o incluir una funcionalidad de búsqueda. Lo que se te ocurra.

Por último, veamos cómo hacer para obtener un documento en específico. Supongamos que queremos agregar una sección que sea "libro destacado".

Agreguemos un nuevo estado:

const [bookOfTheWeek, setBookOfTheWeek] = useState(null)

Agreguemos una nueva llamada a la base de datos en nuestro efecto:

  useEffect(() => {
    const book = db.collection('books').doc('0');
    book.get().then(doc => {
      if (doc.exists) {
        setBookOfTheWeek(doc.data());
      }
    });
  }, []);

En este caso en lugar de llamar a toda la colección books, usamos la propiedad "doc" para llamar a un documento en específico, que identificamos con el id que tiene en Firestore. Chequeamos que el documento exista, y si es asi, lo guardamos en nuestro estado. Ahora podemos usarlo en nuestro JSX:

  return (
    <section>
      <h2>Libro de la semana</h2>
      {bookOfTheWeek && (
        <article>
          <h2>{bookOfTheWeek.title}</h2>
          <h3>{bookOfTheWeek.author}</h3>
          <p>{bookOfTheWeek.year}</p>
        </article>
      )}
    </section>
  );

Con esto ya tenés las bases para poder mostrarle la información obtenida al usuario, filtrarla o mostrar un solo elemento, de acuerdo a tus necesidades. Podés leer más sobre como acceder a la información que tenés guardada en firestore aquí.

Recordá que tu configuración solo va a funcionar por 30 días, y que no es recomendable que deployes este proyecto ya que aún esta en test mode. Para que tu base de datos sea segura, deberemos agregarle reglas que controlen quien puede acceder a la información. Eso lo veremos en una próxima entrega, pero podes aprender sobre el tema en la documentación de firebase.

Si tenés dudas o consultas, más que feliz de orientarte. Sentite libre de contactarme por tu medio favorito.

Notas

  1. Si seguís esta guia y querés escalarla, importar Firebase en cada uno de tus componentes no es buena idea. Mi recomendación es que uses useContext para poner Firebase en tu componente de mayor jerarquía.