Operações CRUD são a base de todas as aplicações, portanto, é essencial tornar-se proficiente nelas quando aprendendo novas tecnologias.
Neste tutorial, você vai aprender a construir uma aplicação CRUD usando React e Convex. Vamos cobrir essas operações construindo um projeto chamado Coleções de livros. Neste projeto, os usuários serão capazes de adicionar livros e atualizar seu status assim que completarem um livro.
Sumário
O que é Convex?
Convex é a plataforma Baas que simplifica o desenvolvimento de backend. Convex vem com uma base de dados em tempo real, e você não precisa se preocupar com a escrita de lógica de servidor separadamente, pois ele fornece métodos para consultar e mutar o banco de dados.
Pré-requisitos
Para seguir este tutorial, você deve saber os fundamentos do React. Eu usarei TypeScript neste projeto, mas é opcional, portanto, você pode seguir com JavaScript também.
Como Configurar Seu Projeto
Crie um separado pasta para o projeto e nomeie-a como desejar – eu vou chamar de Livros. Vamos configurar o Convex e o React nesta pasta.
Você pode criar um aplicativo React usando este comando:
npm create vite@latest my-app -- --template react-ts
Se você quiser trabalhar com JavaScript, então remova o ts
no final. É assim:
npm create vite@latest my-app -- --template react
Como Configurar o Convex
Temos que instalar o Convex na mesma pasta. Você pode fazer isso usando este comando:
npm install convex
Agora, execute npx convex dev
. Se você estiver fazendo isso pela primeira vez, deve pedir autenticação. Caso contrário, deve pedir o nome do projeto.
Você pode visitar o painel do Convex para ver os projetos que você criou.
Agora que nós temos o Convex e o aplicativo React configurados, precisamos conectar o backend do Convex à aplicação React.
No src/main.tsx, envolva seu componente App
com o 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 você configurou o Convex, um arquivo .env.local
foi criado. Você pode ver sua URL de backend nesse arquivo.
Na linha abaixo, nós instanciamos o cliente React Convex com a URL.
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
Como criar o esquema
Em seu diretório de projeto principal, você deveria ver a pasta convex. Aqui nós lidaremos com as consultas e mutações de banco de dados.
Crie um arquivo schema.ts na pasta 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(),
}),
});
Você pode definir um esquema para o seu documento com defineSchema
e criar uma tabela com defineTable
. O Convex fornece essas funções para definir um esquema e criar uma tabela.
v
é o validador de tipo, ele é usado para fornecer tipos para cada dado que você adiciona à tabela.
Para esse projeto, já que é um aplicativo de coleção de livros, a estrutura terá title
, author
e isCompleted
. Você pode adicionar mais campos.
Agora que você definiu seu esquema, vamos configurar a interface básica em React.
Como criar a interface de usuário
Na pasta src, crie uma pasta chamada component e um arquivo Home.tsx. Aqui, você pode definir a 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;
Você pode criar seu componente da forma que desejar. Eu adicionei dois campos de entrada title
e author
, e um botão submit
. Esta é a estrutura básica. Agora podemos criar métodos CRUD no backend.
Como Criar Funções CRUD
Na pasta convex, você pode criar um arquivo separado queries.ts para as funções CRUD.
Criar Função
No arquivo convex/queries.ts:
Você pode definir uma função createBooks
. Você pode usar a função mutation
do Convex para criar, atualizar e excluir dados. A leitura de dados ficará sob query
.
A função mutation
espera esses argumentos:
-
agrs
: os dados que precisamos armazenar no banco de dados. -
handler
: manipula a lógica para armazenar dados no banco de dados. Ohandler
é uma função assíncrona e tem dois argumentos:ctx
eargs
. Aqui,ctx
é o objeto de contexto que usaremos para lidar com as operações de banco de dados.
Você usará o método insert
para inserir novos dados. O primeiro parâmetro do insert
é o nome da tabela e o segundo é os dados que precisam ser inseridos.
Finalmente, você pode retornar os dados do banco de dados.
Aqui está o 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;
},
});
Leitura de Função
No arquivo 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();
},
});
Nessa operação de leitura, nós usamos a função interna query
do Convex. Aqui, o args
estará vazio já que nós não estamos obtendo nenhum dado do usuário. Similarmente, a função handler
é assíncrona e usa o objeto ctx
para consultar o banco de dados e retornar os dados.
Função de Atualização
No arquivo convex/queries.ts:
Crie uma função updateStatus
. Nós vamos atualizar somente o status isCompleted
.
Aqui, você precisa obter o ID do documento e o status do usuário. No args
, nós definiremos id
e isCompleted
, que virão do usuário.
Na função handler
, nós usaremos o método patch
para atualizar os dados. O método patch
espera dois argumentos: o primeiro é o ID do documento e o segundo é os dados atualizados.
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"
},
});
Função de Exclusão
No arquivo convex/queries.ts:
Crie uma função deleteBooks
e use a função mutation
. Precisaremos do ID do documento para deletar. No args
, defina um ID. No handler
, use o método delete
do objeto ctx
e passe o ID. Isso fará o delete do 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";
},
});
Até agora, você concluiu as funções CRUD no backend. Agora precisamos fazer com que funcione na UI. Vamos voltar para o React.
Atualize a UI
Você já criou alguma UI básica na aplicação React, juntamente com alguns campos de entrada. Vamos atualizá-la.
Em 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;
Agora podemos usar as funções de backend API usando api
de Convex. Como você pode ver, chamamos duas funções de API: você pode usar useQuery
se você vai ler dados e useMutation
se você quer mudar dados. Agora neste arquivo, estamos fazendo duas operações que são criar e ler.
Obtivemos todos os dados usando este método:
const books = useQuery(api.queries.getBooks);
O array de objetos será armazenado na variável books.
Obtivemos a função de criação do backend com esta linha de código:
const createBooks = useMutation(api.queries.createBooks);
Como Usar a Função de Criação na UI
Vamos usar a função de criação na UI.
Como os campos de entrada estão dentro da tag form
, vamos usar o atributo onSubmit
para tratar a submissão do formulário.
//In the 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));
};
Ao clicar em enviar, ele dispara a função handleSubmit
.
Nós usamos a createBooks
para passar o title
e o author
do estado. A função de ponto final é assíncrona, portanto, nós podemos usar a handleSubmit
como assíncrona ou usar .then
. Eu usei o método .then
para lidar com dados assíncronos.
Você pode criar um componente separado para exibir os dados fetchados do banco de dados. Os dados retornados estão no Home.tsx, portanto, nós passaremos os dados para o componente Books.tsx como props.
No 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>
);
};
No componente Books.jsx, você pode exibir dados do banco de dados e lidar com a funcionalidade de atualização e exclusão de registros.
Vamos passar por cada uma destas funcionalidades passo a passo.
Como Exibir os Dados
Você pode obter os dados passados como um prop no componente Home.tsx
. Se você estiver usando TypeScript, eu defini um tipo para o objeto que é retornado da consulta. Você pode ignorar isso se estiver usando JavaScript.
Crie `books.types.ts`:
export type book = {
_id: string,
title: string,
author: string,
isCompleted: boolean
}
Você pode usar a função map
para exibir os dados.
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 é a estrutura básica. Exibimos o título, o autor e o status, juntamente com um botão de atualizar e excluir.
Agora, vamos adicionar as 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 é todo o código do componente. Deixe-me explicar o que fizemos.
Primeiro, precisamos alternar a atualização, então definimos a função handleClick
e passamos um ID de documento para ela.
//handleClick
const handleClick = (id: string) => {
setId(id);
setUpdate(!update);
};
Na handleClick
, você pode atualizar o estado do ID e alternar o estado de atualização, assim quando clicado, ele alternará a entrada de atualização e com outro clique, ele fechará.
A seguir, temos o handleUpdate
. Precisamos do ID de documento para atualizar os dados, então passamos o objeto de evento e o ID de documento. Para obter o 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);
};
Nós precisamos usar o useMutation
para obter a função updateStatus
. Passar o ID e o status concluído para a função e manipular a parte assíncrona usando .then
Para a função de excluir, o ID de documento é suficiente. Exatamente como no anterior, chame a função de excluir usando o useMutation
e passe o ID para ela.
Então passe o ID de documento e manipule a 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));
};
Estilização
Finalmente, o que resta é adicionar algum estilo. Eu adicionei alguns estilos básicos. Se o livro não for concluído, estará em vermelho, e se o livro for concluído, estará em verde.
Aqui está a captura de tela:
É isso aí pessoal!!
Você pode ver o código completo no meu repositório: convex-curd
Resumo
Neste artigo, implementamos as operações CRUD (Criar, Ler, Atualizar e Excluir) construindo uma aplicação de coleções de livros. Começamos configurando o Convex e o React e escrevendo a lógica CRUD.
Este tutorial abrangeu tanto o frontend quanto o backend, mostrando como construir uma aplicação sem servidor.
Você pode encontrar o código completo aqui: convex-curd
Se houver algum erro ou dúvida, contate-me no LinkedIn, Instagram.
Obrigado por ler!
Source:
https://www.freecodecamp.org/news/build-crud-app-react-and-convex/