CRUD操作はすべてのアプリケーションの基盤であり、新しい技術を学ぶ際にはそれに熟练することが基本です。
このチュートリアルで、ReactとConvexを使用してCRUDアプリケーションを構築する方法を学びます。ここでは、本のコレクションというプロジェクトを作成し、ユーザーが本を読み終えると、本のステータスを更新できるように追加します。
目次
Convexとは何か?
ConvexはBaasプラットフォームであり、バックエンド開発を簡素化します。Convexにはリアルタイム数据库が含まれており、データベースのクエリや変更を行うための方法が提供されているため、別途サーバー側のロジックの書き込みは不要です。
前提条件
このチュートリアルに従うためには、Reactの基本を知る必要があります。このプロジェクトではTypeScriptを使用しますが、選択的なものであり、JavaScriptだけでも従っていただけます。
プロジェクトの設定方法
プロジェクト用の別のフォルダを作成し、好みの名前にしてください。私はBooksと名付けます。そのフォルダにConvexとReactを設定します。
このようなコマンドを使用してReactアプリを作成することができます。
npm create vite@latest my-app -- --template react-ts
JavaScriptだけを使用している場合は、最後のts
を取り除くだけです。すなわち:
npm create vite@latest my-app -- --template react
Convexの設定方法
Convexを同一フォルダにインストールする必要があります。そのためのコマンドはこうです。
npm install convex
次に、npx convex dev
を実行します。初めてこの操作を行う場合は、認証を求めるはずです。そうでない場合は、プロジェクト名を求めるはずです。
あなたは作成したプロジェクトを見ることができます。
ConvexとReactアプリを設定した後、ConvexのバックエンドをReactアプリに接続する必要があります。
src/main.tsxで、App
コンポーネントを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>
);
Convexを設定したとき、.env.local
が作成されました。このファイルでバックエンドのURLを見ることができます。
次の行で、URLを使用してReact Convexクライアントをインスタンス化します。
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
スキーマの作成方法
主要なプロジェクトディレクトリにconvexディレクトリがあるはずです。データベースのクエリと変数をここで処理します。
schema.tsファイルを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(),
}),
});
ドキュメントのスキーマをdefineSchema
で定義し、defineTable
でテーブルを作成することができます。Convexはスキーマの定義とテーブルの作成に必要な機能を提供しています。
v
は型検証器で、テーブルに追加する各データに型を提供するために使用されます。
このプロジェクトは本のコレクションアプリであるため、構造はtitle
、author
、isCompleted
があります。追加のフィールドを追加することもできます。
スキーマを定義したら、Reactで基本のUIを設定してみましょう。
UIの作成方法
srcフォルダー内に、componentという名前のフォルダーとHome.tsxという名前のファイルを作成します。ここで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;
あなたは自由にコンポーネントを作成することができます。私はtitle
、author
の2つの入力フィールドとsubmit
按钮を追加しました。これは基本的な構造です。今後、バックエンドでCRUDメソッドを作成することができます。
CRUD関数の作成方法
convexフォルダー内で、CRUD関数用のqueries.tsファイルを別に作成することができます。
関数の作成
在convex/queries.ts中:
你可以定义一个名为createBooks
的函数。您可以使用Convex的mutation
函数来创建、更新和删除数据。数据的读取将属于query
。
mutation
函数期待这些参数:
-
agrs
: 我们需要存储在数据库中的数据。 -
handler
: 处理将日期存储在数据库中的逻辑。handler
是一个异步函数,它有两个参数:ctx
和args
。这里,ctx
是我们要用来处理数据库操作的上下文对象。
新しいデータを挿入する際には、`insert`メソッドを使用します。`insert`の最初の引数はテーブル名で、第二の引数は插入する必要があるデータです。
最後に、データベースからデータを返すことができます。
以下はコードです。
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;
},
});
読み取り関数
在`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();
},
});
この読み取り操作で、Convexの内蔵`query`関数を使用しました。ここで、`args`は空であることを想定しており、用户からデータを取得していません。同様に、`handler`関数は异步であり、`ctx`オブジェクトを使用してデータベースをクエリーし、データを返すことができます。
更新関数
在`convex/queries.ts`中:`updateStatus`関数を作成します。ここでは、`isCompleted`ステータスのみを更新します。
ここで、ドキュメントIDとステータスを用户から取得する必要があります。`args`には、用户から来る`id`と`isCompleted`を定義します。
`handler`で、データを更新する`patch`メソッドを使用します。`patch`メソッドには2つの引数を期待します:最初の引数はドキュメントの`id`で、第二の引数は更新後のデータです。
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"
},
});
削除関数
在`convex/queries.ts`中:
deleteBooks
関数を作成し、mutation
関数を使用します。削除するドキュメントのIDが必要です。args
でIDを定義し、handler
でctx
オブジェクトのdelete
メソッドを使用し、IDを渡します。これにより、ドキュメントを削除します。
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";
},
});
今まで、バックエンドのCRUD機能を完了しました。次に、UIにも適用する必要があります。Reactに飛び戻りましょう。
UIの更新
Reactアプリで既に基本的なUIといくつかの入力フィールドを作成しました。これらを更新しましょう。
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;
Convexからapi
を使用して、バックエンドAPI関数を利用できます。ご覧のように、私たちは2つのAPI関数を呼び出しています。データを読む場合はuseQuery
を使用し、データを変更する場合はuseMutation
を使用します。このファイルで、作成と読み取りの2つの操作を行っています。
この方法で全てのデータを取得しました:
const books = useQuery(api.queries.getBooks);
オブジェクトの配列はbooks変数に格納されます。
このコード行で、バックエンドからcreate機能を取得しました:
const createBooks = useMutation(api.queries.createBooks);
UIでCreate機能を使用する方法
UIでcreate機能を使用しましょう。
入力フィールドがform
タグに含まれているため、これらのフォームの送信を処理するためにonSubmit
属性を使用します。
//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));
};
送信を押すと、handleSubmit
関数をトリガーにする。
私たちはcreateBooks
を使用して、状態からtitle
とauthor
を渡す。エンドポイント関数は非同期なので、handleSubmit
を非同期として使用したり、.then
を使用したりすることができます。私は.then
方法を使用して非同期データを処理しました。
データベースから取得したデータを表示するためには、別のコンポーネントを作成することができます。返されるデータはHome.tsxにありますので、Books.tsxコンポーネントにpropsとしてデータを渡します。
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>
);
};
Books.jsxコンポーネントで、データベースからデータを表示し、レコードの更新と削除の機能を処理することができます。
これらの機能のそれぞれについて、一つ一つ手順を踏まえていきましょう。
データの表示方法
Home.tsx
コンポーネントで、propsとして渡されたデータを取得することができます。TypeScriptを使用している場合、クエリから返されるオブジェクトについて型を定義しました。JavaScriptを使用している場合はこれを無視することができます。
`books.types.ts`を作成します。
export type book = {
_id: string,
title: string,
author: string,
isCompleted: boolean
}
データを表示するためにmap
関数を使用することができます。
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>
);
};
これは基本的な構造です。私たちはタイトル、著者、ステータスを表示し、更新と削除のボタンも表示しました。
今、機能を追加しましょう。
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>
);
};
これはすべてのコンポーネントのコードです。私が何をしたのか説明しましょう。
まず、更新を切り替える必要があります。そのために、私はhandleClick
関数を定義し、文書IDをそこに渡しました。
//handleClick
const handleClick = (id: string) => {
setId(id);
setUpdate(!update);
};
在handleClick
中、ID状態を更新し、更新状態を切り替えることができます。これにより、クリック時に更新入力を切り替え、もう一回クリックすると閉じます。
次に、handleUpdate
があります。データを更新するために、私はイベントオブジェクトと文書IDをそれに渡しました。入力を取得するために、私は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);
};
useMutation
を使用してupdateStatus
関数を取得する必要があります。IDと完了状態を関数に渡し、.then
を使用して非同期部分を処理します。
削除機能には、文書IDだけが十分です。前のものと同様に、useMutation
を使用して削除関数を呼び出し、そしてIDを渡します。
次に、文書IDと約束を処理します。
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));
};
スタイリング
最後に、いくつかのスタイリングを追加する必要があります。私は基本的なスタイリングを追加しました。本が完了していない場合、赤色になり、本が完了している場合、緑色になります。
以下はスクリーンショットです。
これで終わりです、みんな!
私のリポジトリに完全なコードを確認できます: convex-curd
要約
この記事では、本のコレクションアプリを構築することでCRUD(Create、Read、Update、Delete)操作を実装しました。ConvexとReactのセットアップを始め、CRUDロジックを書くことから始めます。
このチュートリアルでは、フロントエンドとバックエンドの両方をカバーし、サーバーレスアプリケーションの構築方法を示しました。
ここで完全なコードを見つけることができます:convex-curd
もし何か間違いや疑問があれば、LinkedInやInstagramでご連絡ください。
お読みいただき、ありがとうございました!
Source:
https://www.freecodecamp.org/news/build-crud-app-react-and-convex/