ReactJS y FaunaDB

Introducción

Fauna DB es una base de datos distribuida y globalmente accesible que ofrece una solución serverless para el almacenamiento de datos. Combina características de bases de datos documentales y relacionales, diseñada específicamente para el desarrollo de aplicaciones modernas.

En este tutorial, aprenderás los conceptos básicos para integrar la base de datos en tu aplicación ReactJS y realizar operaciones CRUD.

Proyecto ReactJS

Vamos a utilizar ViteJS para crea el proyecto ReactJS, abre una consola y escribe el siguiente comando:

npm create vite@latest reactjs-fauna -- --template react

Este comando generará un directorio reactjs-fauna , entra e instala las dependencias necesarias con el comando npm install . Cuando finalice, puedes ejecutar el comando npm run dev para correr el proyecto.

Instalación y configuración de TailwindCSS

Para añadirle estilos a nuestro proyecto, vamos a utilizar el framework TailwindCSS. Instalamos las dependencias necesarias con el siguiente comando:

npm install -D tailwindcss postcss autoprefixer

Para inicializar TailwindCSS corremos el comando:

npx tailwindcss init -p

Esto creará dos ficheros, tailwind.config.js y postcss.config.js . Abre el fichero tailwind.config.js y actualiza como sigue:

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

Ahora abre el fichero src/index.css y añade las directivas de tailwind como sigue:

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

Ahora asegúrate que el archivo CSS esté importado en el archivo principal del proyecto (en este caso src/main.jsx)

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css' // IMPORTAMOS LOS ESTILOS DE TAILWINDCSS

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Finalmente vamos a instalar lucide-react que es un biblioteca de íconos para react, basada en el proyecto Lucide. Ofrece una colección de iconos de código abierto, simples y consistentes que pueden ser fácilmente integrados en aplicaciones React.

Lo instalas con el siguiente comando:

npm install lucide-react

Ya tenemos todo listo para comenzar a crear los componentes de nuestro proyecto.

Componente Form

El componente Form.jsx nos permitirá añadir tareas a la lista de tareas. Crea un fichero src/components/Form.jsx y añade el siguiente código:

import { Plus } from 'lucide-react';

export default function Form({ newTodo, setNewTodo, addTodo }) {
  return (
    <form onSubmit={addTodo} className="flex gap-2 mb-6">
      <input
        type="text"
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder="¿Qué necesitas hacer?"
        className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
      />
      <button
        type="submit"
        className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2"
      >
        <Plus size={20} />
        Agregar
      </button>
    </form>
  );
}

Vamos a explicar las funciones más destacadas:

Componente Form:

Es una función que recibe tres propiedades: newTodo, setNewTodo, y addTodo.

  • newTodo: Representa el valor actual del texto de la nueva tarea.

  • setNewTodo: Función que se usa para actualizar el valor de newTodo.

  • addTodo: Función que se ejecuta cuando el usuario envía el formulario, agregando una nueva tarea a la lista.

Formulario HTML con manejo de eventos:

<form onSubmit={addTodo} className="flex gap-2 mb-6">
  • Se usa un elemento <form> que ejecuta la función addTodo cuando se envía (evento onSubmit). La función addTodo se encargará de agregar una nueva tarea a la lista de tareas.

Campo de entrada de texto:

<input
  type="text"
  value={newTodo}
  onChange={(e) => setNewTodo(e.target.value)}
  placeholder="¿Qué necesitas hacer?"
  className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
  • El valor de este campo (value={newTodo}) está ligado a la propiedad newTodo, permitiendo el control del estado.

  • El evento onChange actualiza el estado (setNewTodo) cada vez que el usuario escribe algo, capturando el valor ingresado.

Hemos finalizado el componente Form.jsx . Continuamos con el componente Todo.jsx.

Componente Todo

El componente Todo.jsx tendrá como función mostrar la información de cada una de las tareas de la lista. Crea un fichero src/components/Todo.jsx y añade el siguiente código:

import { Trash2, Check } from 'lucide-react';

export default function Todo({ todo, toggleTodo, deleteTodo }) {
  return (
    <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
      <div className="flex items-center gap-3">
        <button
          onClick={() => toggleTodo(todo.id)}
          className={`w-6 h-6 rounded-full border-2 flex items-center justify-center
            ${todo.completed 
              ? 'bg-green-500 border-green-500 text-white' 
              : 'border-gray-400'
            }`}
        >
          {todo.completed && <Check size={16} />}
        </button>
        <span className={`text-gray-800 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
          {todo.text}
        </span>
      </div>
      <button
        onClick={() => deleteTodo(todo.id)}
        className="text-red-500 hover:text-red-700 focus:outline-none"
      >
        <Trash2 size={20} />
      </button>
    </div>
  );
}

A continuación se explica las funcionalidades más destacadas:

Propiedades recibidas:

  • todo: Un objeto que representa una tarea específica.

  • toggleTodo: Función que se ejecuta cuando el usuario marca o desmarca la tarea como completada.

  • deleteTodo: Función que se ejecuta cuando el usuario elimina la tarea.

Botón para marcar como completada:

<button
  onClick={() => toggleTodo(todo.id)}
  className={`w-6 h-6 rounded-full border-2 flex items-center justify-center
    ${todo.completed 
      ? 'bg-green-500 border-green-500 text-white' 
      : 'border-gray-400'
    }`}
>
  {todo.completed && <Check size={16} />}
</button>
  • Este botón permite al usuario marcar la tarea como completada o pendiente.

  • onClick={() => toggleTodo(todo.id)}: Al hacer clic, se ejecuta la función toggleTodo pasando el id de la tarea, lo que permite alternar el estado de completado.

  • Estilización dinámica:

    • Si la tarea está completada (todo.completed === true):

      • Se aplica fondo y borde verde (bg-green-500, border-green-500), y el ícono Check (una marca de verificación) es visible.
    • Si la tarea no está completada:

      • Solo se muestra un borde gris (border-gray-400), sin ícono dentro del botón.

Texto de la tarea:

<span className={`text-gray-800 ${todo.completed ? 'line-through text-gray-500' : ''}`}>
  {todo.text}
</span>
  • Muestra el texto de la tarea (todo.text).

  • Si la tarea está completada (todo.completed), se aplica el estilo de tachado (line-through) y el color del texto se vuelve gris claro (text-gray-500), indicando visualmente que la tarea ya ha sido realizada. Si no está completada, el texto se mantiene en gris oscuro (text-gray-800).

Botón para eliminar la tarea:

<button
  onClick={() => deleteTodo(todo.id)}
  className="text-red-500 hover:text-red-700 focus:outline-none"
>
  <Trash2 size={20} />
</button>
  • Este botón permite eliminar la tarea.

  • onClick={() => deleteTodo(todo.id)}: Al hacer clic, se ejecuta la función deleteTodo con el id de la tarea, eliminándola de la lista.

Con esto hemos finalizado con el componente Todo.jsx, ahora debemos actualizar el componente principal de la aplicación App.jsx.

Componente TodoApp

Este componente es la base de la aplicación de lista de tareas. Abre el fichero src/App.jsx y actualízalo con el siguiente código:

import { useState } from 'react';
import Form from './components/Form';
import Todo from './components/Todo';

export default function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');

  const addTodo = (e) => {
    e.preventDefault();
    if (newTodo.trim()) {
      setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
      setNewTodo('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
        <h1 className="text-2xl font-bold text-gray-800 mb-6">Lista de Tareas</h1>

        <Form newTodo={newTodo} setNewTodo={setNewTodo} addTodo={addTodo} />

        <div className="space-y-3">
          {todos.map(todo => (
            <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />
          ))}
        </div>

        {todos.length === 0 && (
          <div className="text-center text-gray-500 mt-6">
            No hay tareas pendientes. ¡Añade una!
          </div>
        )}
      </div>
    </div>
  );
}

A continuación explicamos sus funcionalidades más destacadas.

Importaciones de componentes y uso de useState:

import { useState } from 'react';
import Form from './components/Form';
import Todo from './components/Todo';
  • Se importa el hook useState para manejar el estado dentro del componente.

  • También se importan dos componentes hijos, Form y Todo, que se encargan de manejar el formulario para agregar tareas y la representación de cada tarea respectivamente.

Estados del componente:

const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
  • todos: Un arreglo que contiene todas las tareas creadas. Cada tarea es un objeto con las propiedades id, text, y completed.

  • newTodo: Almacena el texto de la nueva tarea que el usuario está escribiendo.

Función addTodo para agregar nuevas tareas:

const addTodo = (e) => {
  e.preventDefault();
  if (newTodo.trim()) {
    setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
    setNewTodo('');
  }
};
  • Esta función se ejecuta cuando se envía el formulario para agregar una nueva tarea.

  • e.preventDefault(): Previene que la página se recargue al enviar el formulario.

  • Validación: Se asegura de que el texto no esté vacío o compuesto solo de espacios en blanco con newTodo.trim().

  • Agregar la tarea: Si el texto es válido, se añade una nueva tarea al arreglo todos. La tarea tiene un id único basado en la fecha actual (Date.now()), el texto (newTodo), y un estado completed: false (indica que la tarea no está completada).

  • Después de agregar la tarea, se limpia el campo de texto con setNewTodo('').

Función toggleTodo para actualizar tareas completadas:

const toggleTodo = (id) => {
    setTodos(
      todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

La función toggleTodo se utiliza para alternar el estado de completado de una tarea en una lista de tareas. Aquí te explico cómo funciona:

  • Parámetro id: La función recibe un parámetro id, que representa el identificador único de la tarea que se desea actualizar.

  • setTodos: Esta es una función que se utiliza para actualizar el estado de las tareas.

  • todos.map: La función map se utiliza para crear un nuevo arreglo de tareas basado en el arreglo existente todos. Para cada tarea en el arreglo, se verifica si su id coincide con el id proporcionado a la función toggleTodo.

  • Condición todo.id === id: Si el id de la tarea actual coincide con el id proporcionado, se crea un nuevo objeto de tarea con todas las propiedades de la tarea actual ({ ...todo }), pero con el estado completed invertido (!todo.completed). Esto significa que si la tarea estaba marcada como completada, ahora estará pendiente, y viceversa.

  • Retorno de map: Si el id no coincide, la tarea se devuelve sin cambios.

Función deleteTodo para eliminar tareas completadas:

const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

La función deleteTodo se utiliza para eliminar una tarea de la lista de tareas:

  • Parámetro id: La función recibe un parámetro id, que representa el identificador único de la tarea que se desea eliminar.

  • setTodos: Esta es una función que se utiliza para actualizar el estado de las tareas.

  • todos.filter: La función filter se utiliza para crear un nuevo arreglo de tareas. Recorre cada tarea en el arreglo todos y aplica una condición.

  • Condición todo.id !== id: La condición verifica si el id de la tarea actual no coincide con el id proporcionado. Si no coincide, la tarea se incluye en el nuevo arreglo, el nuevo arreglo contiene todas las tareas excepto aquella cuyo id coincide con el id proporcionado, efectivamente eliminando esa tarea de la lista.

Incorporación de los componentes Form y Todo :

Para incorporar el formulario lo hacemos con la siguiente línea:

<Form newTodo={newTodo} setNewTodo={setNewTodo} addTodo={addTodo} />

Este componente es responsable de renderizar el formulario que permite a los usuarios agregar nuevas tareas. Recibe tres propiedades para gestionar el estado y las acciones del formulario.

  • newTodo={newTodo}: Representa el estado actual del texto de la nueva tarea que el usuario está escribiendo. Se utiliza para controlar el valor del campo de entrada de texto en el formulario.

  • setNewTodo={setNewTodo}: Se utiliza para actualizar el estado de newTodo cada vez que el usuario escribe en el campo de entrada. Esto permite que el componente mantenga el control del valor del campo de texto.

  • addTodo={addTodo}: Se ejecuta cuando el usuario envía el formulario. Su propósito es agregar la nueva tarea a la lista de tareas, utilizando el texto almacenado en newTodo.

Para incorporar el componente Todo lo hacemos con la siguiente línea:

        <div className="space-y-3">
          {todos.map(todo => (
            <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />
          ))}
        </div>

Aquí se explica cada parte del código:

  • {todos.map(todo => ( ... ))}: Itera sobre un arreglo llamado todos. Por cada elemento todo en el arreglo, se ejecuta la función de flecha (todo => ( ... )), que devuelve un componente Todo.

  • <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} deleteTodo={deleteTodo} />: Este componente Todo se le pasan varias propiedades:

    • key={todo.id}: La propiedad key es un identificador único para cada elemento en la lista, lo cual ayuda a React a identificar qué elementos han cambiado, agregado o eliminado.

    • todo={todo}: Pasa el objeto todo actual como una propiedad al componente Todo.

    • toggleTodo={toggleTodo}: Pasa la función toggleTodo como una propiedad, que se utiliza dentro del componente Todo para alternar el estado de completado de la tarea.

    • deleteTodo={deleteTodo}: Pasa la función deleteTodo como una propiedad, que se utiliza dentro del componente Todo para eliminar la tarea.

Con esto hemos finalizado la construcción del front-end de la aplicación Todo. Ahora vamos a realizar la conexión con la base de datos en FaunaDB.

FaunaDB - Creación de la base de datos

Ingresa a tu cuenta en FaunaDB, en el panel de control de tu cuenta, haz clic en el botón “Create Database” y ponle un nombre. Yo lo nombré “todoapp”.

Una vez creada la base de datos podemos crear la colección que contendrá las tareas. Para eso en el “shell” escribe la siguiente instrucción Collection.create({ name: "Todo" }) :

Continuamos con la creación del 'key secret’ que permitirá a nuestra aplicación realizar las operaciones CRUD.

FaunaDB - Creación del ‘key secret’

Para crear la clave secreta, hay que dirigirse a la pestaña ‘Keys’, hacer clic en el botón ‘Create Key’. Aparecerá una ventana, en ella elige un rol, ponle un nombre a la clave y haz clic en ‘Save’.

La clave aparecerá sólo una vez, una vez que cierres la ventana ya no podrás acceder a ella. Copia la clave, vuelve al editor y en la raíz del proyecto crea un fichero .env para variables de entorno. Crea una nueva variable VITE_REACT_APP_FAUNA_SECRET y pega la clave.

VITE_REACT_APP_FAUNA_SECRET=fnAFw2Cje_AAQn7vMLAlaUHnoXXlyfZROpb0zTO3

(Tanto la base de datos como la clave están eliminados al momento de la publicación del artículo).

Continuamos con la instalación y configuración de la biblioteca fauna .

Instalación y configuración de fauna package

Este paquete permitirá conectar la aplicación con la base de datos y realizar las operaciones de lectura y escritura. Realiza la instalación con el siguiente comando:

npm install fauna

Ahora crea un fichero src/lib/fauna.js que se encargará de realizar la conexión con la base de datos.

import { Client, fql } from "fauna"

// Asegúrate de crear un archivo .env para almacenar tu clave secreta
const client = new Client({
  secret: import.meta.env.VITE_REACT_APP_FAUNA_SECRET,
})

export default client

Ahora en el mismo directorio crea un fichero src/lib/todoDb.js que tendrá las funciones CRUD de la aplicación.

import client from './fauna'
import { fql } from 'fauna'

export const todoDb = {
  // Obtener todas las tareas
  getAllTodos: async () => {
    try {
      const result = await client.query(fql`
        Todo.all()
      `)
      return result.data
    } catch (error) {
      console.error('Error al obtener las tareas:', error)
      return []
    }
  },

  // Crear una nueva tarea
  createTodo: async (todoData) => {
    try {
      const result = await client.query(fql`
        Todo.create({
          text: ${todoData.text},
          completed: false
        })
      `)
      return result.data
    } catch (error) {
      console.error('Error al crear la tarea:', error)
      throw error
    }
  },

  // Actualizar una tarea
  updateTodo: async (todoId, updates) => {
    try {
      const result = await client.query(fql`
        Todo.byId(${todoId})!.update({
          completed: ${updates.completed}
        })
      `)
      return result.data
    } catch (error) {
      console.error('Error al actualizar la tarea:', error)
      throw error
    }
  },

  // Eliminar una tarea
  deleteTodo: async (todoId) => {
    try {
      await client.query(fql`
        Todo.byId(${todoId})!.delete()
      `)
      return true
    } catch (error) {
      console.error('Error al eliminar la tarea:', error)
      throw error
    }
  }
}

Explicación de las funciones principales:

  1. getAllTodos (Obtener todas las tareas):

    • Consulta la base de datos y obtiene todas las tareas almacenadas.

    • Si hay un error, lo captura y devuelve un array vacío.

  2. createTodo (Crear una nueva tarea):

    • Recibe un objeto con los datos de una nueva tarea.

    • Inserta la tarea en la base de datos con el texto proporcionado y el estado completed: false por defecto.

    • Si ocurre un error, lo captura y lo lanza.

  3. updateTodo (Actualizar una tarea):

    • Recibe el ID de la tarea y los datos a actualizar.

    • Modifica el estado de la tarea (completed) en la base de datos.

    • Captura y lanza errores si ocurren.

  4. deleteTodo (Eliminar una tarea):

    • Recibe el ID de una tarea y la elimina de la base de datos.

    • Si la operación tiene éxito, devuelve true.

    • Captura errores y los lanza.

A continuación debemos actualizar el componente src/App.jsx como sigue:

import { useState, useEffect } from 'react';
import Form from './components/Form';
import Todo from './components/Todo';
import { todoDb } from './lib/todoDb';

export default function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTodo, setNewTodo] = useState('');
  const [loading, setLoading] = useState(true);

  // Cargar tareas al iniciar
  useEffect(() => {
    loadTodos();
  }, []);

  const loadTodos = async () => {
    try {
      const { data } = await todoDb.getAllTodos();
      setTodos(data);
    } catch (error) {
      console.error('Error al cargar las tareas:', error);
    } finally {
      setLoading(false);
    }
  };

  const addTodo = async (e) => {
    e.preventDefault();
    if (newTodo.trim()) {
      try {
        const newTodoData = await todoDb.createTodo({ text: newTodo });
        setTodos([...todos, newTodoData]);
        setNewTodo('');
      } catch (error) {
        console.error('Error al añadir la tarea:', error);
      }
    }
  };

  const toggleTodo = async (id) => {
    try {
      const todoToUpdate = todos.find(todo => todo.id === id);
      const updatedTodo = await todoDb.updateTodo(id, { 
        completed: !todoToUpdate.completed 
      });
      setTodos(todos.map(todo =>
        todo.id === id ? updatedTodo : todo
      ));
    } catch (error) {
      console.error('Error al actualizar la tarea:', error);
    }
  };

  const deleteTodo = async (id) => {
    try {
      await todoDb.deleteTodo(id);
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (error) {
      console.error('Error al eliminar la tarea:', error);
    }
  };

  if (loading) {
    return <div className="text-center mt-8">Cargando...</div>;
  }

  return (
    <div className="min-h-screen bg-gray-100 py-8">
      <div className="max-w-md mx-auto bg-white rounded-lg shadow-lg p-6">
        <h1 className="text-2xl font-bold text-gray-800 mb-6">Lista de Tareas</h1>

        <Form newTodo={newTodo} setNewTodo={setNewTodo} addTodo={addTodo} />

        <div className="space-y-3">
          {todos.map(todo => (
            <Todo 
              key={todo.id} 
              todo={todo} 
              toggleTodo={toggleTodo} 
              deleteTodo={deleteTodo} 
            />
          ))}
        </div>

        {todos.length === 0 && (
          <div className="text-center text-gray-500 mt-6">
            No hay tareas pendientes. ¡Añade una!
          </div>
        )}
      </div>
    </div>
  );
}

De esta manera tenemos integrado las funciones CRUD del cliente de Fauna a nuestra aplicación.

Probar la aplicación

Para probar la aplicación corre el comando npm run dev y prueba añadir, actualizar y eliminar una tarea, los datos deberían verse reflejados en la base de datos en fauna:

Conclusión

En este tutorial, hemos construido una aplicación TODO con ReactJS y la hemos integrado con FaunaDB para manejar el almacenamiento de tareas. Aprendimos a conectar nuestra aplicación con la base de datos, realizar operaciones CRUD (Crear, Leer, Actualizar y Eliminar tareas) y gestionar la comunicación con el backend de forma eficiente.

FaunaDB nos ofrece una base de datos serverless, escalable y fácil de manejar con consultas FQL, lo que hace que sea una excelente opción para aplicaciones modernas con React. Además, este enfoque nos permite trabajar con una arquitectura sin servidor, reduciendo la necesidad de infraestructura compleja.

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!👋😊

0
Subscribe to my newsletter

Read articles from Carlos Alberto Alegre directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Carlos Alberto Alegre
Carlos Alberto Alegre

Soy Carlos Alberto Alegre, analista programador y desarrollador web front-end y back-end. Me considero un autodidacta entusiasta y disfruto explorando y aprendiendo nuevas tecnologías para mantenerme al tanto de las últimas tendencias en diseño y desarrollo web.