Les opérations CRUD sont la base de chaque application, il est donc essentiel de maîtriser celles-ci lors de l’apprentissage de nouvelles technologies.

Dans ce tutoriel, vous apprendrez comment construire une application CRUD en utilisant React et Convex. Nous aborderons ces opérations en construisant un projet appelé Collections de Livres. Dans ce projet, les utilisateurs pourront ajouter des livres et mettre à jour leur statut une fois qu’ils auront terminé un livre.

Table des matières

Qu’est-ce que Convex ?

Convex est la plateforme Baas qui simplifie le développement du back-end. Convex est fourni avec une base de données en temps réel, et vous n’avez pas besoin d’écrire de logique serveur séparée parce qu’il fournit des méthodes pour interroger et modifier la base de données.

Prérequis

Afin de suivre ce tutoriel, vous devez connaître les fondamentaux de React. J’utiliserais TypeScript dans ce projet, mais c’est facultatif, vous pouvez donc aussi suivre avec JavaScript.

Comment Configurer Votre Projet

Créez un dossier séparé pour le projet et nommez-le comme vous le souhaitez – je le nommerai Books. Nous configurons Convex et React dans ce dossier.

Vous pouvez créer un application React à l’aide de cette commande :

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

Si vous voulez travailler avec JavaScript, ensuite retirez le ts à la fin. C’est-à-dire :

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

Comment Configurer Convex

Nous devons installer Convex dans le même dossier. Vous pouvez faire cela en utilisant cette commande :

npm install convex

Ensuite, exécutez npx convex dev. Si vous le faites pour la première fois, il devrait demander l’authentification. Sinon, il devrait demander le nom du projet.

Vous pouvez visiter le tableau de bord Convex pour voir les projets que vous avez créés.

Maintenant que nous avons mis en place Convex et l’application React, nous devons connecter le backend Convex à l’application React.

Dans le fichier src/main.tsx, enveloppez votre composant App avec 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>
);

Lorsque vous avez configuré Convex, un fichier .env.local a été créé. Vous pouvez voir votre URL de backend dans ce fichier.

Dans la ligne ci-dessous, nous avons instancié le client React Convex avec l’URL.

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

Comment Créer le Schéma

Dans votre répertoire principal de projet, vous devriez voir le répertoire convex. Nous gérerons les requêtes de base de données et les mutations ici.

Créez un fichier schema.ts dans le dossier 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(),
  }),
});

Vous pouvez définir un schéma pour votre document avec defineSchema et créer une table avec defineTable. Convex fournit ces fonctions pour définir un schéma et créer une table.

v est le validateur de type, il est utilisé pour fournir les types pour chaque donnée que vous ajoutez à la table.

Pour ce projet, car c’est une application de collection de livres, la structure sera composée de title, author et isCompleted. Vous pouvez ajouter d’autres champs.

Maintenant que vous avez défini votre schéma, passons à la mise en place de l’UI de base en React.

Comment Créer l’UI

Dans le dossier src, créez un dossier nommé component et un fichier Home.tsx. Ici, vous pouvez définir l’UI.

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;

Vous pouvez créer votre composant comme vous le souhaitez. J’ai ajouté deux champs d’entrée title, author et un bouton submit. C’est la structure de base. Maintenant, nous pouvons créer des méthodes CRUD dans l’arrière-plan.

Comment Créer des Fonctions CRUD

Dans le dossier convex, vous pouvez créer un fichier distinct queries.ts pour les fonctions CRUD.

Créer une Fonction

Dans convex/queries.ts :

Vous pouvez définir une fonction createBooks. Vous pouvez utiliser la fonction mutation de Convex pour créer, modifier et supprimer des données. Lire les données se situera sous query.

La fonction mutation attend ces arguments :

  • agrs : les données que nous avons besoin d’enregistrer dans la base de données.

  • handler : gère la logique pour enregistrer les dates dans la base de données. La handler est une fonction asynchrone et elle a deux arguments : ctx et args. Ici, ctx est l’objet de contexte que nous utilisons pour gérer les opérations de base de données.>

Vous utiliserez la méthode insert pour insérer de nouvelle donnée. Le premier paramètre de la méthode insert est le nom de la table et le second est les données qui doivent être insérées.

Enfin, vous pouvez retourner les données depuis la base de données.

Voici le code :

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;
  },
});

Fonction de lecture

Dans 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();
  },
});

Dans cette opération de lecture, nous avons utilisé la fonction intégrée query de Convex. Ici, args sera vide car nous ne récupérons pas de données de l’utilisateur. De même, la fonction handler est asynchrone et utilise l’objet ctx pour interroger la base de données et retourner les données.

Fonction de mise à jour

Dans convex/queries.ts :

Créer une fonction updateStatus. Nous ne mettrons à jour que le statut isCompleted.

Ici, vous devez obtenir l’ID du document et le statut de l’utilisateur. Dans args, nous définirons id et isCompleted, qui viendront de l’utilisateur.

Dans la fonction handler, nous utiliserons la méthode patch pour mettre à jour les données. La méthode patch attend deux arguments : le premier argument est l’ID du document et le second est les données mises à jour.

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"
  },
});

Fonction de suppression

Dans convex/queries.ts :

Créez une fonction deleteBooks et utilisez la fonction mutation. Nous aurons besoin de l’ID du document à supprimer. Dans les args, définissez un ID. Dans le handler, utilisez la méthode delete de l’objet ctx et passez l’ID. Cela supprimera le document.

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";
  },
});

A présent, vous avez terminé les fonctions CRUD dans le backend. Maintenant, nous avons besoin de les rendre fonctionnels dans l’UI. Revenons à React.

Mettez à jour l’UI

Vous avez déjà créé une interface utilisateur basique dans l’application React, avec quelques champs d’entrée. Mettez-les à jour.

Dans 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;

Nous pouvons maintenant utiliser les fonctions d’API du backend en utilisant api de Convex. Comme vous pouvez le voir, nous avons appelé deux fonctions API : vous pouvez utiliser useQuery si vous devez lire des données et useMutation si vous voulez modifier des données. Dans ce fichier, nous effectuons deux opérations : créer et lire.

Nous avons obtenu toutes les données en utilisant cette méthode :

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

Le tableau d’objets sera stocké dans la variable books.

Nous avons récupéré la fonction de création du backend avec cette ligne de code :

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

Comment utiliser la fonction de création dans l’UI

Utilisons la fonction de création dans l’UI.

Puisque les champs d’entrée sont dans l’attribut form, nous utiliserons l’attribut onSubmit pour gérer le soumission du formulaire.

//Dans 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));
  };

Lorsque vous cliquez sur Envoyer, il déclenche la fonction handleSubmit.

Nous avons utilisé la fonction createBooks pour passer title et author depuis l’État. La fonction de point de terminaison est asynchrone, donc nous pouvons utiliser handleSubmit comme asynchrone ou utiliser .then. J’ai utilisé la méthode .then pour gérer les données asynchrones.

Vous pouvez créer un composant séparé pour afficher les données récupérées depuis la base de données. Les données renvoyées sont dans Home.tsx, donc nous passerons les données au composant Books.tsx en props.

Dans 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>
  );
};

Dans le composant Books.jsx, vous pouvez afficher les données de la base de données et gérer la fonctionnalité de mise à jour et de suppression de records.

Allons-y pas à pas pour chacune de ces fonctionnalités.

Comment afficher les données

Vous pouvez obtenir les données passées en props dans le composant Home.tsx. Si vous utilisez TypeScript, j’ai défini un type pour l’objet retourné depuis la requête. Vous pouvez ignorer cela si vous utilisez JavaScript.

Créer `books.types.ts` :

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

Vous pouvez utiliser la fonction map pour afficher les données.

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>
  );
};

C’est la structure de base. Nous avons affiché le titre, l’auteur et l’état, ainsi que des boutons de mise à jour et de suppression.

Maintenant, ajoutons les fonctionnalités.

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>
  );
};

C’est tout le code du composant. Expliquez-moi ce que nous avons fait.

Tout d’abord, nous devons basculer l’update, donc nous avons défini la fonction handleClick et nous avons transmis un ID de document à cette fonction.

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

Dans la fonction handleClick, vous pouvez mettre à jour l’état de l’ID et basculer l’état d’update pour que, lorsque cliqué, il bascule l’input d’update, et sur un autre clic, il le ferme.

Ensuite, nous avons la fonction handleUpdate. Nous avons besoin de l’ID du document pour mettre à jour les données, donc nous avons transmis l’objet événement ainsi que l’ID du document. Pour obtenir l’input, nous pouvons utiliser 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);
  };

Nous devons utiliser useMutation pour obtenir la fonction updateStatus. Transmettre l’ID et l’état terminé à la fonction et gérer la partie asynchrone en utilisant .then

Pour la fonction suppression, l’ID du document est suffisant. Comme pour la suppression précédente, appelez la fonction suppression en utilisant useMutation et transmettez l’ID à cela.

Ensuite, transmettez l’ID du document et gérez la promesse.

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));
 };

Styling

Enfin, ce qui reste est d’ajouter quelques styles. J’ai ajouté quelques styles de base. Si le livre n’est pas terminé, il sera en rouge, et si le livre est terminé, il sera en vert.

Voici la capture d’écran :

C’est fini les gars !!

Vous pouvez vérifier mon dépôt pour le code complet : convex-curd

Résumé

Dans cet article, nous avons implémenté les opérations CRUD (Create, Read, Update, and Delete) en construisant une application de collections de livres. Nous commençons par configurer Convex et React et écrivons la logique CRUD.

Ce tutoriel a couvert à la fois le frontend et le backend, montrant comment construire une application sans serveur.

Vous pouvez trouver le code complet ici : convex-curd

Si vous trouvez des erreurs ou des doutes, vous pouvez me contacter sur LinkedIn, Instagram.

Merci d’avoir lu !