Autoguardado de formulario con React y Firebase

Autoguardado de formulario con React y Firebase

Introducción

Si alguna vez has utilizado un CMS habrás visto como a medida que escribes tu contenido, se va guardando automáticamente. Esta funcionalidad vamos a recrear en este tutorial, utilizando React como front-end y Firebase como back-end.

Proyecto

El proyecto consiste en un formulario con dos campos, título y contenido. A medida que llenemos los campos, de forma automática se disparará una función que se encargará de guardar el contenido en una base de datos en Firebase. Una barra de estado nos indicará el status del proceso de guardado.

Consideraciones

El tutorial es enteramente práctico, por lo que no cubriremos conceptos teóricos y los aspectos básicos de las herramientas que vamos a utilizar. Por ello es recomendable tener conocimiento del lenguaje de programación javascript, nociones básicas de React y alguna experiencia en la utilización de Firebase.

Instalación y configuración

Comencemos creando la aplicación React. Lo llamé "react-autosave".

npx create-react-app react-autosave

Cuando finalice la instalación entra al directorio del código fuente y crea una carpeta para el fichero de configuración de firebase, react-autosave/src/firebase/firebase-config.js .

Ahora ve a la consola de firebase y crea un nuevo proyecto. Haz clic en "Agregar proyecto" y dale un nombre. Yo lo llamé "react-firebase-autosave". Cuando finalice la creación haz clic en Cloud Firebase y crea un nueva base de datos. Activa el modo prueba.

Luego te pidirá habilitar la ubicación donde se almacenará los datos de Cloud Firestore. Dale habilitar. Accederás a tu panel de control. Ahora hay que registrar tu app para el proyecto. Haz clic en el ícono de configuración y elige "Configuración de proyecto".

Ve a la sección "Tus apps", aquí debemos registrar la app para nuestra web.

Una vez que registres el nombre de la app tendrás acceso a la configuración del SDK de firebase para tu proyecto.

Esta configuración es la que debemos pegar en el fichero de configuración de firebase que creamos anteriormente, react-autosave/src/firebase/firebase-config.js . Primero instalamos el SDK de firebase con npm i firebase .

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  authDomain: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  projectId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  storageBucket: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  messagingSenderId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

Además de inicializar la app, accedemos a la base de datos de Cloud Firestore y lo guardamos en const db , luego lo exportamos. Ya tenemos todo listo para comenzar a trabajar con Cloud Firestore. Ahora vamos a crear el formulario.

Formulario

Para los estilos del proyecto vamos a utilizar TailwindCSS. La documentación para la instalación y configuración de TailwindCSS en React podés encontrarla AQUÍ.

Dentro del directorio fuente crea una carpeta para el componente del formulario, react-autosave/src/Form/Form.js .

function Form() {
    return (
        <>
            <section className="text-white w-[600px] p-4">
                <header className="text-white text-4xl text-center py-4">Form Autosave</header>
                <form className="mt-5">
                    <div>
                        <label className="w-full">Title:</label>
                        <input type="text" name="title" placeholder="Title" className="w-full p-2 rounded bg-gray-700 my-2"/>
                    </div>
                    <div>
                        <label className="w-full">Content:</label>
                        <textarea name="content" rows="10" cols="30" placeholder="Content" className="w-full p-2 rounded bg-gray-700 my-2"/>
                    </div>
                </form>
            </section>
        </>
    )
}

 export default Form;

Modificamos el fichero react-autosave/src/App.js para incorporar el formulario.

import Form from "./Form/Form";

function App() {
  return (
    <div className="min-h-screen max-w-full bg-slate-900 flex justify-center items-center">
      <Form />
    </div>
  );
}

export default App;

Estado del Formulario

Usaremos useState para controlar el estado del formulario y vamos a crear una función handleChange para actualizar los valores.

import { useState } from "react";

function Form() {
    const [post, setPost] = useState({
        id: '',
        title: '',
        content: ''
    });
    const handleChange = (name, value) => {
        setPost({...post, [name]: value});
    }
    return (
        <>
            <section className="text-white w-[600px] p-4">
                <header className="text-white text-4xl text-center py-4">Form Autosave</header>
                <form className="mt-5">
                    <div>
                        <label className="w-full">Title:</label>
                        <input type="text" name="title" placeholder="Title" className="w-full p-2 rounded bg-gray-700 my-2"
                        onChange={(e) => handleChange(e.target.name, e.target.value)}
                        />
                    </div>
                    <div>
                        <label className="w-full">Content:</label>
                        <textarea name="content" rows="10" cols="30" placeholder="Content" className="w-full p-2 rounded bg-gray-700 my-2"
                        onChange={(e) => handleChange(e.target.name, e.target.value)}
                        />
                    </div>
                </form>
            </section>
        </>
    )
}

 export default Form;

Con esta función podemos actualizar el estado haciendo uso del evento onChange . Sin embargo, esto se ejecuta cada vez que tipeamos en el formulario, compromentiendo el rendimiento de la app. Podemos retrasar la llamada a la función handleChange con el método debounce . Para ello utilizaremos la librería radash. Para instalarlo ejecutamos el comando npm i radash . Ahora modificamos la función handleChange como sigue.

//Importamos la función debounce de radash
import { debounce } from "radash";
....
//Modificamos la función hanbleChange
const handleChange = debounce({ delay: 700 }, (name, value) => {
       setPost({...post, [name]: value});
    });

El método debounce recibe un objeto con la opción { delay: 700 } , el cual indica los milisegundos de retraso para llamar a la función. Como segundo parámetro recibe la función que queremos ejecutar una vez pasado el "delay". Ahora la función handleChange solo se ejecutará cuando dejemos de tipear.

Ahora debemos darle un id al objeto post que vamos a guardar. Lo haremos con la librería uuid . Lo instalamos con el comando npm i uuid .

La propiedad id del objeto solo se debe establecer en la primera llamada, de lo contrario estaría generando uno nuevo en cada llamada. Lo hacemos como sigue.

//Importamos la función para generar el id
import { v4 as uuidv4 } from "uuid";

//Modificamos la función handleChange
const handleChange = debounce({ delay: 700 }, (name, value) => {
        if(!post.id){
            setPost({...post, id: uuidv4(), [name]: value });
        } else {
            setPost({...post, [name]: value });
        }
    });

Si post.id está vacío, entoces le asigna un valor, de lo contrario solo actualiza los demás valores, manteniendo el id . Este bloque podemos reducirlo haciendo uso del operador condicional ternario como sigue.

const handleChange = debounce({ delay: 700 }, (name, value) => {
        !post.id ? setPost({...post, id: uuidv4(), [name]: value}) :                     setPost({...post, [name]: value});
    });

Ahora debemos incorporar la función para guardar los datos en nuestra base de datos de Cloud Firestore.

Cloud Firestore

Para añadir la funcionalidad de guardado vamos a utilizar el "hook" useEffect . Lo añadimos como sigue.

// Importamos el hook useEffect
import { useEffect, useState } from "react";

// Importamos la instancia de la base de datos Cloud Firestore
import { db } from "../firebase/firebase-config";
// Importamos las funciones para establecer un doc nuevo
import { doc, setDoc } from "firebase/firestore";
...
useEffect(() => {
        if (post.id) {
            setDoc(doc(db, "posts", post.id), { title: post.title, content: post.content});
        }
    }, [post]);

Este "hook" se ejecutará cuando se detecte un cambio en el estado de post . Con setDoc podemos escribir un documento proporcionando una referencia hacia dicho documento. Si el documento existe, lo actualizará, si no, lo creará. Con doc especificamos dicha referencia. Recibe la instancia de la base de datos, el nombre de la colección ("posts") y un identificador de documento, en nuestro caso usamos post.id .

Como el estado inicial de post.id es una cadena vacía, la función doc disparará un error. Para evitarlo establecemos la condición if para que la función solo corra cuando post.id no esté vacío.

Con esto la funcionalidad está terminada, pero vamos a añadir la barra de estado que nos indicará el progreso del proceso.

Barra de Estado

Para la barra vamos a usar una librería de íconos animados para React llamado UseAnimations. Para instalarlo corremos el comando npm i react-useanimations .

Dentro del directorio fuente crea una carpeta para la barra de estado react-autosave/src/statusbar/StatusBar.js .

import UseAnimations from "react-useanimations";
import loading from "react-useanimations/lib/loading";
import checkbox from "react-useanimations/lib/checkBox";

function StatusBar(props) {
    return (
        <>
            {props.sending &&
            <div className="flex justify-center items-center">
                <span className="text-green-700">Sending...</span><UseAnimations animation={loading} size={36} strokeColor={"#15803d"} />
            </div>
            }
            {!props.sending &&
            <div className="flex justify-center items-center gap-1">
                <span className="text-green-700">Saved</span><UseAnimations reverse={true} animation={checkbox} size={36} strokeColor={"#15803d"} autoplay={true} />
            </div>
            }
        </>
    )
}

export default StatusBar;

En primer lugar importamos el componente UseAnimations . Seguido importamos dos animaciones, loading para cuando los datos son enviados, y checkbox para cuando los datos han sido guardados.

Dentro del componente StatusBar utilizamos un renderizado condicional. Si props.sending es verdadero, entonces renderiza la animación loading , de lo contrario renderizará la animación checkbox .

Volviendo al componente del formulario, importamos la barra de estado. También vamos a establecer un nuevo estado sending para determinar si los datos están siendo enviados.

// Importamos la barra de estado
import StatusBar from "../statusbar/StatusBar";

// Creamos un estado para el status del envío de datos
const [status, setStatus] = useState({ sending: false });

// Modificamos la función para incorporar el estado del envío de datos
useEffect(() => {
        if (post.id) {
            setStatus({sending: true});
            setDoc(doc(db, "posts", post.id), { title: post.title, content: post.content})
                .then(() => {
                    setStatus({sending: false});
                });
        }
    }, [post]);

Antes del envío de datos actualizamos el status a verdadero. Cuando el envío de datos finaliza el status es actualizado a falso.

Ahora incorporamos la barra al render.

<div className="py-2 flex justify-between items-center gap-1 border-y-2 border-green-700">
   <span className="text-green-700">STATUS:</span> { post.id &&
                        <StatusBar sending={status.sending}/>
                    }
</div>

El componente StatusBar solo se renderiza si post.id no está vacío. Además en los props del componente pasamos el status .

El código completo del componente Form.js .

import { useEffect, useState } from "react";
import { debounce } from "radash";
import { v4 as uuidv4 } from "uuid";
import { db } from "../firebase/firebase-config";
import { doc, setDoc } from "firebase/firestore";
import StatusBar from "../statusbar/StatusBar";

function Form() {
    const [post, setPost] = useState({
        id: '',
        title: '',
        content: ''
    });

    const [status, setStatus] = useState({ sending: false });

    const handleChange = debounce({ delay: 700 }, (name, value) => {
        !post.id ? setPost({...post, id: uuidv4(), [name]: value}) : setPost({...post, [name]: value});
    });

    useEffect(() => {
        if (post.id) {
            setStatus({sending: true});
            setDoc(doc(db, "posts", post.id), { title: post.title, content: post.content})
                .then(() => {
                    setStatus({sending: false});
                });
        }
    }, [post]);

    return (
        <>
            <section className="text-white w-[600px] p-4">
                <header className="text-white text-4xl text-center py-4">Form Autosave</header>
                <div className="py-2 flex justify-between items-center gap-1 border-y-2 border-green-700">
                    <span className="text-green-700">STATUS:</span> { post.id &&
                        <StatusBar sending={ status.sending }/>
                    }

                </div>
                <form className="mt-5">
                    <div>
                        <label className="w-full">Title:</label>
                        <input type="text" name="title" placeholder="Title" className="w-full p-2 rounded bg-gray-700 my-2"
                        onChange={(e) => handleChange(e.target.name, e.target.value)}
                        />
                    </div>
                    <div>
                        <label className="w-full">Content:</label>
                        <textarea name="content" rows="10" cols="30" placeholder="Content" className="w-full p-2 rounded bg-gray-700 my-2"
                        onChange={(e) => handleChange(e.target.name, e.target.value)}
                        />
                    </div>
                </form>
            </section>
        </>
    )
}

 export default Form;

Ahora probemos la app. Corre el comando npm start .

Inicialmente la barra de estado está vacío.

Cuando tipeamos el título del post podemos ver como se activa la barra de estado.

Cuando finaliza el proceso de guardado de datos en la base de datos de Cloud Firestore, la barra de estado se actualiza.

Podemos verificar como los datos han sido guardados en la base de datos correctamente.

Finalizamos con el tutorial.

Conclusión

Hemos aprendido a integrar React con Cloud Firestore de Firebase. Como ejecutar una función de forma automática para guardar los datos y como incorporar una barra de estado que nos indica el progreso de guardado.

Puedes encontrar el proyecto completo en mi repositorio de GitHub haciendo clic AQUÍ.

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊