Las operaciones CRUD son la base de cada aplicación, por lo que es fundamental dominarlas cuando se están aprendiendo nuevas tecnologías.

En este tutorial, aprenderás cómo construir una aplicación CRUD usando React y Convex. Recorremos estas operaciones construyendo un proyecto llamado Colecciones de Libros. En este proyecto, los usuarios podrán agregar libros y actualizar su estado una vez que complete un libro.

Tabla de Contenido

¿Qué es Convex?

Convex es la Plataforma Baas que simplifica el desarrollo de backend. Convex viene con una base de datos en tiempo real, y no necesitas preocuparte de escribir lógica de servidor separadamente porque proporciona métodos para consultar y mutar la base de datos.

Prerrequisitos

Para seguir este tutorial, debes saber los conceptos básicos de React. En este proyecto utilizaré TypeScript, pero es opcional, así que también puedes seguir con JavaScript.

Cómo Configurar tu Proyecto

Crea una carpeta separada para el proyecto y nombre la que desees – Yo la llamaré Libros. Configuraremos Convex y React en esa carpeta.

Puedes crear una aplicación de React utilizando este comando:

npm create vite@latest my-app -- --template react-ts

Si quieres trabajar con JavaScript, entonces elimina el ts al final. Eso sería:

npm create vite@latest my-app -- --template react

Cómo Configurar Convex

Tenemos que instalar Convex en la misma carpeta. Puedes hacerlo utilizando este comando:

npm install convex

Después, ejecuta npx convex dev. Si estás haciendo esto por primera vez, debería pedirte autenticación. En caso contrario, debería pedirte el nombre del proyecto.

Puedes visitar el panel de control de Convex para ver los proyectos que has creado.

Ahora que hemos configurado Convex y la aplicación de React, necesitamos conectar el backend de Convex a la aplicación de React.

En el src/main.tsx, envuelve tu componente App con el ConvexReactClient:

import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import "./index.css"

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

createRoot(document.getElementById("root")!).render(
  <ConvexProvider client={convex}>
    <App />
  </ConvexProvider>
);

Cuando configuras Convex, se creó un .env.local. Puedes ver tu URL de backend en ese archivo.

En la línea de abajo, instanciamos el cliente de React Convex con la URL.

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);

Cómo crear el esquema

En tu directorio principal del proyecto, deberías ver la carpeta convex. Allí manejaremos las consultas y mutaciones de la base de datos.

Crea un archivo schema.ts en la carpeta convex:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  books: defineTable({
    title: v.string(),
    author: v.string(),
    isCompleted: v.boolean(),
  }),
});

Puedes definir un esquema para tu documento con defineSchema y crear una tabla con defineTable. Convex proporciona estas funciones para definir un esquema y crear una tabla.

v es el validador de tipos, se utiliza para proporcionar tipos para cada dato que agregues a la tabla.

Para este proyecto, ya que es una aplicación de colección de libros, la estructura tendrá title, author y isCompleted. Puedes agregar más campos.

Ahora que has definido tu esquema, vamos a configurar la interfaz de usuario básica en React.

Cómo crear la interfaz de usuario

En el directorio src, cree un directorio llamado component y un archivo Home.tsx. Aquí, puede definir la interfaz de usuario.

import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

Puede crear sus componentes como desee. Agrego dos campos de entrada title, author, y un botón submit. Esta es la estructura básica. Ahora podemos crear métodos CRUD en el backend.

Cómo Crear Funciones CRUD

En el directorio convex, puede crear un archivo separado queries.ts para las funciones CRUD.

Crear Función

En convex/queries.ts:

Puede definir una función createBooks. Puede usar la función mutation de Convex para crear, actualizar y eliminar datos. La lectura de datos estará bajo query.

La función mutation espera estos argumentos:

  • agrs: los datos que necesita almacenar en la base de datos.

  • handler: maneja la lógica para almacenar la fecha en la base de datos. La handler es una función asíncrona y tiene dos argumentos: ctx y args. Aquí, ctx es el objeto de contexto que usaremos para manejar las operaciones de base de datos.

Usted utilizará el método insert para insertar nuevos datos. El primer parámetro del insert es el nombre de la tabla y el segundo son los datos que necesitan ser insertados.

Finalmente, puede devolver los datos de la base de datos.

Aquí está el código:

import { mutation} from "./_generated/server";
import { v } from "convex/values";

export const createBooks = mutation({
  args: { title: v.string(), author: v.string() },
  handler: async (ctx, args) => {
    const newBookId = await ctx.db.insert("books", {
      title: args.title,
      author: args.author,
      isCompleted: false,
    });
    return newBookId;
  },
});

Función de lectura

En convex/queries.ts:

import { query } from "./_generated/server";
import { v } from "convex/values";

//read
export const getBooks = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("books").collect();
  },
});

En esta operación de lectura, hemos usado la función integrada query de Convex. Aquí, args estará vacío ya que no estamos obteniendo datos del usuario. Similarmente, la función handler es asíncrona y utiliza el objeto ctx para consultar la base de datos y devolver los datos.

Funcción de actualización

En convex/queries.ts:

Cree una función updateStatus. Sólo vamos a actualizar el estado isCompleted.

En este caso, necesita obtener la ID del documento y el estado del usuario. En los args, definiremos id y isCompleted, que vendrán del usuario.

En el handler, usaremos el método patch para actualizar los datos. El método patch espera dos argumentos: el primer argumento es la id del documento y el segundo es los datos actualizados.

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//update
export const updateStatus = mutation({
  args: { id: v.id("books"), isCompleted: v.boolean() },
  handler: async (ctx, args) => {
    const { id } = args;
    await ctx.db.patch(id, { isCompleted: args.isCompleted });
    return "updated"
  },
});

Funcción de eliminación

En convex/queries.ts:

Cree una función deleteBooks y use la función mutation. Necesitaremos el ID del documento que se va a eliminar. En los args, define un ID. En el handler, use el método delete del objeto ctx y pase el ID. Esto eliminará el documento.

import { mutation } from "./_generated/server";
import { v } from "convex/values";

//delete
export const deleteBooks = mutation({
  args: { id: v.id("books") },
  handler: async (ctx, args) => {
    await ctx.db.delete(args.id);
    return "deleted";
  },
});

Desde ahora, ha completado las funciones CRUD en el backend. Ahora necesitamos hacer que funcione en la UI. Vamos a regresar a React.

Actualizar la UI

Usted ya creó una UI básica en la aplicación React, junto con algunos campos de entrada. Vamos a actualizarlo.

En src/component/Home.tsx:

import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Books } from "./Books";
import { useState } from "react";
import "../styles/home.css";

const Home = () => {
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");
  const books = useQuery(api.queries.getBooks);
  const createBooks = useMutation(api.queries.createBooks);

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };
  return (
    <div className="main-container">
      <h1>Book Collections</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          name="title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="book title"
        />
        <br />
        <input
          type="text"
          name="author"
          value={author}
          onChange={(e) => setAuthor(e.target.value)}
          placeholder="book author"
        />
        <br />
        <input type="submit" />
      </form>
      {books ? <Books books={books} /> : "Loading..."}
    </div>
  );
};

export default Home;

Ahora podemos usar las funciones de API del backend utilizando api desde Convex. Como puede ver, llamamos dos funciones de API: puede usar useQuery si va a leer datos y useMutation si quiere cambiar datos. En este archivo, estamos realizando dos operaciones: crear y leer.

Obtuve todos los datos usando este método:

 const books = useQuery(api.queries.getBooks);

El arreglo de objetos se almacenará en la variable books.

Obtuve la función crear del backend con esta línea de código:

const createBooks = useMutation(api.queries.createBooks);

Cómo Usar la Función Crear en la UI

Vamos a usar la función crear en la UI.

Dado que los campos de entrada están en la etiqueta form, usaremos el atributo onSubmit para manejar la presentación del formulario.

//En Home.tsx

const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    createBooks({ title, author })
      .then(() => {
        console.log("created");
        setTitle("");
        setAuthor("");
      })
      .catch((err) => console.log(err));
  };

Al hacer clic en enviar, dispara la función handleSubmit.

Usamos la función createBooks para pasar el title y el author del estado. La función de extremo es asíncrona, así que podemos usar la handleSubmit como asíncrona o usar .then. Utilice el método .then para manejar los datos asíncronos.

Puedes crear un componente separado para mostrar los datos obtenidos de la base de datos. Los datos devueltos están en Home.tsx, así que pasaremos los datos al componente Books.tsx como props.

En Books.tsx:

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

En el componente Books.jsx, puedes mostrar datos de la base de datos y manejar la funcionalidad para actualizar y eliminar registros.

Vamos a recorrer cada una de estas características paso a paso.

Cómo mostrar los datos

Puedes obtener los datos pasados como una propiedad en el componente Home.tsx. Si estás usando TypeScript, yo he definido un tipo para el objeto que se devuelve de la consulta. Puedes ignorar esto si estás usando JavaScript.

Crear `books.types.ts`:

export type book = {
    _id: string,
    title: string,
    author: string,
    isCompleted: boolean
}

Puedes usar la función map para mostrar los datos.

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

Esta es la estructura básica. Mostramos el título, el autor y el estado, junto con un botón de actualizar y eliminar.

Ahora, agreguemos las funcionalidades.

import { useState } from "react";
import { book } from "../types/book.type";
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import { Id } from "../../convex/_generated/dataModel";
import "../styles/book.css";

export const Books = ({ books }: { books: book[] }) => {
  const [update, setUpdate] = useState(false);
  const [id, setId] = useState("");

  const deleteBooks = useMutation(api.queries.deleteBooks);
  const updateStatus = useMutation(api.queries.updateStatus);

  const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

  const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
  };

  const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

  return (
    <div>
      {books.map((data: book, index: number) => {
        return (
          <div
            key={data._id}
            className={`book-container ${data.isCompleted ? "completed" : "not-completed"}`}
          >
            <h3>Book no: {index + 1}</h3>
            <p>Book title: {data.title}</p>
            <p>Book Author: {data.author}</p>
            <p>
              Completed Status:{" "}
              {data.isCompleted ? "Completed" : "Not Completed"}
            </p>
            <button onClick={() => handleClick(data._id)}>Update</button>
            {id === data._id && update && (
              <>
                <form onSubmit={(e) => handleUpdate(e, data._id)}>
                  <select name="completed">
                    <option value="true">Completed</option>
                    <option value="false">Not Completed</option>
                  </select>
                  <input type="submit" />
                </form>
              </>
            )}
            <button onClick={() => handleDelete(data._id)}>delete</button>
          </div>
        );
      })}
    </div>
  );
};

Este es todo el código del componente. Explicaré lo que hemos hecho.

Primero, necesitamos cambiar el estado de actualización, así que hemos definido la función handleClick y le hemos pasado un ID de documento.

//handleClick
 const handleClick = (id: string) => {
    setId(id);
    setUpdate(!update);
  };

En la función handleClick puedes actualizar el estado del ID y cambiar el estado de actualización para que cambie la entrada de actualización al hacer clic, y al hacer clic de nuevo, se cerrará.

A continuación, tenemos handleUpdate. Necesitamos el ID de documento para actualizar los datos, así que le hemos pasado el objeto de evento y el ID de documento. Para obtener el input, podemos usar FormData.

const updateStatus = useMutation(api.queries.updateStatus);

const handleUpdate = (e: React.FormEvent<HTMLFormElement>, id: string) => {
    e.preventDefault();
    const formdata = new FormData(e.currentTarget);
    const isCompleted: boolean =
      (formdata.get("completed") as string) === "true";
    updateStatus({ id: id as Id<"books">, isCompleted })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
    setUpdate(false);
  };

Necesitamos usar useMutation para obtener la función updateStatus. Pasa el ID y el estado finalizado a la función y maneja la parte asíncrona usando .then

Para la función de eliminación, el ID de documento es suficiente. Al igual que el anterior, llama a la función de eliminación usando useMutation y le pasas el ID.

Luego pasa el ID de documento y maneja la promesa.

const deleteBooks = useMutation(api.queries.deleteBooks);

const handleDelete = (id: string) => {
    deleteBooks({ id: id as Id<"books"> })
      .then((mess) => console.log(mess))
      .catch((err) => console.log(err));
 };

Estilizado

Finalmente, lo que queda es agregar algún estilizado. Agregué algunos estilos básicos. Si el libro no ha sido finalizado, estará en rojo, y si el libro ha sido finalizado, estará en verde.

Aquí está la captura de pantalla:

¡Eso es todo, chicos!

Puedes ver mi repositorio para el código completo: convex-curd

Resumen

En este artículo, implementamos las operaciones CRUD (Create, Read, Update, and Delete) construyendo una aplicación de colecciones de libros. Comenzamos configurando Convex y React, y escribiendo la lógica CRUD.

Este tutorial cubrió tanto el frontend como el backend, demostrando cómo construir una aplicación sin servidor.

Puede encontrar el código completo aquí: convex-curd

Si hay algún error o duda, póngase en contacto conmigo en LinkedIn, Instagram.

Gracias por leer!