Le operazioni CRUD sono la base di ogni applicazione, quindi è essenziale diventare efficienti nelle stesse quando si impara nuove tecnologie.

In questo tutorial, imparerete come costruire un’applicazione CRUD usando React e Convex. Vedremo queste operazioni costruendo un progetto chiamato Raccolte di Libri. In questo progetto, gli utenti saranno in grado di aggiungere libri e aggiornare il loro stato una volta completato un libro.

Indice

Cosa è Convex?

Convex è la piattaforma Baas che semplifica la programmazione del backend. Convex include una database reale-time, e non dovrai preoccuparti di scrivere logiche di server separate, poiché fornisce metodi per la ricerca e la modifica del database.

Prerequisiti

Per seguire questo tutorial, devi conoscere i fondamenti di React. In questo progetto userò TypeScript, ma è facoltativo, quindi puoi seguire anche con JavaScript.

Come Configurare il tuo Progetto

Creare una cartella separata per il progetto e dargli un nome a tuo piacimento – la mia chiamerò Books. Configureremo Convex e React in quella cartella.

Puoi creare un’app React usando questo comando:

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

Se vuoi lavorare con JavaScript, allora togliti il ts alla fine. Quindi:

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

Come Configurare Convex

Dobbiamo installare Convex nella stessa cartella. Puoi farlo usando questo comando:

npm install convex

Successivamente, esegui npx convex dev. Se lo stai facendo per la prima volta, dovrebbe chiederti l’autenticazione. Altrimenti, dovrebbe chiedere il nome del progetto.

Puoi visitare il pannello di controllo Convex per visualizzare i progetti che hai creato.

Ora che abbiamo impostato Convex e l’app React, dobbiamo connettere il backend di Convex all’app React.

In src/main.tsx, avvolgi il tuo componente App con 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>
);

Quando hai impostato Convex, è stato creato un file .env.local. Puoi vedere il tuo URL di backend in quel file.

In questa riga, abbiamo creato l’istanza del client React Convex con l’URL.

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

Come creare il Schema

Nel tuo directory del progetto principale, dovresti vedere la directory convex. qui si gestiscono le query al database e le mutazioni.

Crea un file schema.ts nella cartella 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(),
  }),
});

Puoi definire un Schema per il tuo documento con defineSchema e creare una tabella con defineTable. Convex fornisce queste funzioni per la definizione di un schema e la creazione di una tabella.

v è il validatore del tipo, viene utilizzato per fornire i tipi per ciascuna informazione che aggiungi alla tabella.

Per questo progetto, poiché è una applicazione per la collezione di libri, la struttura avrà title, author e isCompleted. Puoi aggiungere più campi.

Ora che hai definito il tuo schema, configura il基本的 UI in React.

Come creare l’UI

Nella cartella src, creare una cartella chiamata component e un file Home.tsx. qui, puoi definire l’interfaccia utente.

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;

Puoi creare il tuo componente come ti pare. Aggiunto due campi di input title, author e un pulsante submit. Questa è la struttura di base. Ora possiamo creare metodi CRUD nel backend.

Come creare funzioni CRUD

Nella cartella convex, puoi creare un file separato queries.ts per le funzioni CRUD.

Creare Funzione

In convex/queries.ts:

Puoi definire una funzione createBooks. Puoi usare la funzione mutation da Convex per creare, aggiornare e eliminare dati. La lettura dei dati verrà sotto query.

La funzione mutation attende questi argomenti:

  • agrs: i dati che dobbiamo memorizzare nel database.

  • handler: gestisce la logica per memorizzare i dati nel database. La handler è una funzione asincrona e ha due argomenti: ctx e args. qui, ctx è l’oggetto contesto che userai per gestire le operazioni del database.

Utilizzerai il metodo insert per inserire nuovi dati. Il primo parametro del insert è il nome della tabella e il secondo sono i dati da inserire.

Infine, puoi restituire i dati dal database.

Ecco il codice:

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

Read Function

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

In questa operazione di read, abbiamo usato la funzione interna query di Convex. Qui, args sarà vuoto poiché non stiamo ricevendo dati dall’utente. Allo stesso modo, la funzione handler è asincrona e utilizza l’oggetto ctx per interrogare il database e restituire i dati.

Update Function

In convex/queries.ts:

Creare una funzione updateStatus. Soltanto aggiornare lo stato isCompleted.

Qui, devi ottenere l’ID del documento e lo stato dall’utente. Nell’args, definiremo id e isCompleted, che verranno forniti dall’utente.

Nel handler, userai il metodo patch per aggiornare i dati. Il metodo patch attende due argomenti: il primo è l’id del documento e il secondo è i dati aggiornati.

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

Delete Function

In convex/queries.ts:

Creare una funzione deleteBooks e usare la funzione mutation. Avremo bisogno dell’ID del documento da eliminare. Nell’args, definire un ID. Nel handler, usare il metodo delete dell’oggetto ctx e passare l’ID. Questo eliminerà il 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";
  },
});

Ad ora, hai completato le funzioni CRUD sul backend. Ora abbiamo bisogno di renderle funzionanti nella UI. Torniamo a React.

Aggiornare l’UI

Hai già creato una UI di base nell’app React, insieme a qualche campo di input. Aggiorniamola.

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

Ora possiamo usare le funzioni API del backend usando api da Convex. Come vedete, abbiamo chiamato due funzioni API: puoi usare useQuery se stai leggendo dati e useMutation se vuoi cambiare dati. In questo file, stiamo facendo due operazioni: create e read.

Hai ottenuto tutti i dati usando questo metodo:

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

L’array di oggetti sarà memorizzato nella variabile books.

Hai ottenuto la funzione create dal backend con questo frammento di codice:

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

Come Utilizzare la Funzione Create nella UI

Usa la funzione create nella UI.

Poiché i campi di input sono all’interno del tag form, userai l’attributo onSubmit per gestire il submit del form.

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

Quando fai clic su “invia”, attiva la funzione handleSubmit.

Usiamo la funzione createBooks per passare il title e l’author dallo stato. La funzione di destinazione è asincrona, quindi possiamo usare la handleSubmit come asincrona o usare .then. Ho usato il metodo .then per gestire i dati asincroni.

Potresti creare un componente separato per mostrare i dati ottenuti dalla base dati. I dati restituiti sono negli Home.tsx, quindi passerò i dati al componente Books.tsx come props.

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

Nel componente Books.jsx puoi mostrare i dati dalla base dati e gestire la funzionalità per aggiornare e eliminare i record.

Facciamo un passo per volta attraverso ognuno di questi feature.

Come mostrare i dati

Puoi ottenere i dati passati come props nel componente Home.tsx. Se stai usando TypeScript, ho definito un tipo per l’oggetto che viene restituito dalla query. Ignorali se stai usando JavaScript.

Creare `books.types.ts`:

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

Puoi usare la funzione map per mostrare i dati.

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

Questa è la struttura di base. Abbiamo mostrato il titolo, l’autore e lo stato, insieme a un pulsante di aggiornamento e di eliminazione.

Ora aggiungiamo le funzionalità.

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

Ecco il codice completo del componente. Spiegherò cosa abbiamo fatto.

Prima di tutto, dobbiamo attivare l’aggiornamento, quindi abbiamo definito la funzione handleClick e l’abbiamo passata un ID documento.

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

Nella funzione handleClick puoi aggiornare lo stato dell’ID e attivare/disattivare l’aggiornamento in modo da attivare l’input di aggiornamento quando viene cliccato e, con un altro clic, lo chiudere.

Successivamente, abbiamo la funzione handleUpdate. Serve l’ID del documento per aggiornare i dati, quindi l’abbiamo passato l’oggetto evento e l’ID del documento. Per ottenere l’input, puoi usare 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);
  };

Dobbiamo usare useMutation per ottenere la funzione updateStatus. Passare l’ID e lo stato completato alla funzione e gestire la parte asincrona usando .then

Per la funzione di eliminazione, l’ID del documento è sufficiente. Come nel caso precedente, chiamare la funzione di eliminazione usando useMutation e passare l’ID.

Poi passare l’ID del documento e gestire la promessa.

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

Stile

A questo punto, ciò che rimane è aggiungere del styling. Ho aggiunto alcuni stili di base. Se il libro non è stato completato, sarà in rosso, se invece è stato completato, sarà in verde.

Ecco la schermata:

Ecco qui ragazzi!!

Puoi controllare il mio repository per il codice completo: convex-curd

Riepilogo

In questo articolo, abbiamo implementato le operazioni CRUD (Create, Read, Update, and Delete) creando una applicazione per le collezioni di libri. Iniziamo impostando Convex e React, e scrivendo la logica CRUD.

Questo tutorial ha coperto sia il frontend che il backend, dimostrando come costruire una applicazione serverless.

Potete trovare il codice completo qui: convex-curd

Se ci sono errori o domande, contattami su LinkedIn, Instagram.

Grazie per aver letto!