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 abordar essas operações construindo um projeto chamado Coleções de Livros. Neste projeto, os usuários poderão adicionar livros e atualizar seu status assim que completarem um livro.
Tabela de Conteúdos
O que é Convex?
Convex é a plataforma Baas que simplifica o desenvolvimento de backend. O Convex vem com uma base de dados em tempo real, e você não precisa se preocupar em escrever lógica de servidor separadamente, pois ele fornece métodos para consultar e modificar o banco de dados.
Pré-requisitos
Para seguir este tutorial, você deve saber os fundamentos do React. Eu usarei TypeScript neste projeto, mas é opcional, então você também pode seguir com JavaScript.
Como Configurar Seu Projeto
Crie uma pasta separada para o projeto e nomeie-a como desejar – eu vou chamar a minha 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 remove o ts
no final. Isso é:
npm create vite@latest my-app -- --template react
Como Configurar o Convex
Nós 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, deveria pedir autenticação. Caso contrário, deveria pedir o nome do projeto.
Você pode visitar o painel de instrumentos Convex para ver os projetos que você criou.
Como nossa configuração do Convex e do aplicativo React já está pronta, agora precisamos conectar o backend do Convex à aplicação React.
No arquivo 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>
);
Ao configurar 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 Schema
No diretório principal do seu projeto, você deveria ver a pasta convex. Aqui nós trataremos 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 Schema para seu documento com defineSchema
e criar uma tabela com defineTable
. O Convex fornece essas funções para a definição de esquema e criação de 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 no React.
Como criar a interface gráfica (UI)?
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 conforme 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 data no banco de dados. Ohandler
é uma função assíncrona e tem dois argumentos:ctx
eargs
. Aqui,ctx
é o objeto de contexto que vamos usar para lidar com as operações de banco de dados.
Você usará o método insert
para inserir novos dados. O primeiro parâmetro no 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;
},
});
Função de Leitura
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();
},
});
Nesta operação de leitura, nós usamos a função interna query
do Convex. Aqui, 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 iremos 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 eliminará o 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 uma UI básica na aplicação React, juntamente com alguns campos de entrada. Vamos atualizá-la.
No 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 API do backend 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ê quiser mudar dados. Agora neste arquivo, estamos fazendo duas operações: 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 este trecho 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 lidar com 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 o createBooks
para passar o title
e o author
do estado. A função de ponto final é assíncrona, então nós podemos usar o handleSubmit
como assíncrono ou usar .then
. Eu usei o método .then
para manipular os 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, então nós vamos passar 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 manipular a funcionalidade para atualizar e excluir 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 para que, 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 exclusão, o ID de documento é suficiente. Tal como no anterior, chame a função de exclusão 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, implementámos 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/