TauriV2 で簡単な TODO アプリを作ってみます。
今回作成したもの。
事前準備
- Rust のインストール
- Node.js のインストール
プロジェクトの作成
以下のコマンドでプロジェクトを作成します。
npm create tauri-app@latest
cd todo
npm install
npm run tauri dev
作成時にはいくつかの質問がありますので、以下のように回答しています。
✔ Project name · todo
✔ Identifier · com.todo.app
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · React - (https://react.dev/)
✔ Choose your UI flavor · JavaScript
コマンド実行後ウィンドウが立ち上がります。ちなみに、ホットリロードが効いているので、コードを修正すると自動でリロードされます。
chakra ui のインストール
今回デザインには Chakra UI を使用します。
npm i @chakra-ui/react @emotion/react
npx @chakra-ui/cli snippet add
main.jsx を以下のように変更し、ChakraProvider を追加します。
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "./components/ui/Provider";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>
);
App.jsx を以下のように変更し、chalkra ui が適用されているか確認します。
import { Button } from "./components/ui/Button";
function App() {
return (
<>
<Button variant="outline">hello world</Button>
</>
);
}
export default App;
Sqlite のインストール
タスクの管理には Sqlite を使用します。追加には以下のコマンドを実行します。
npm run tauri add sql
cd src-tauri
cargo add tauri-plugin-sql --features sqlite
cargo コマンドは、src-tauri ディレクトリで実行する必要があるため、注意してください。
データベースの作成
src-tauri に sql ファイルを作成し、以下のように記述します。
CREATE TABLE task (
-- 自動増分の主キー
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- タスクのタイトル
title TEXT,
-- 作成日時、デフォルトは現在時刻
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- 更新日時、デフォルトは現在時刻
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
-- 論理削除フラグ、デフォルトは0(削除されていない)
deleted INTEGER DEFAULT 0
);
sql ファイルを読み込むために、以下のように migratiosn.rs を作成します。
use tauri_plugin_sql::{Migration, MigrationKind};
pub fn get_migrations() -> Vec<Migration> {
vec![Migration {
version: 1,
description: "Initial migration",
sql: include_str!("./000_init.sql"),
kind: MigrationKind::Up,
}]
}
tauri のエントリポイント用のファイル libs.rs を以下のように変更します。今回は mydatabase.db という名前でデータベースを作成します。
mod migrations;
use migrations::migrations::get_migrations;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(
tauri_plugin_sql::Builder::default()
.add_migrations("sqlite:mydatabase.db", get_migrations())
.build(),
)
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
最後に、tauri.conf.json を以下のように変更します。
{
// ...
"plugins": {
"sql": {
"preload": ["sqlite:mydatabase.db"]
}
}
}
また、フロントエンドから Sqlite を操作するための権限を付与します。権限は src-tauri/capabilities/default.json
に記述します。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:default",
"sql:default",
"sql:allow-execute"
]
}
CRUD の実装
本記事ではmフロントエンドの javascript から Sqlite を操作しますが、フロントエンドから DB を直接操作するのはセキュリティ上の問題があるため、実際のアプリケーションでは Rust 側にロジックを隠蔽した方が良いかもしれません。
クエリの実装
CRUD のためのクエリを以下のように実装します。
import { SqlBase } from "./base";
import Database from "@tauri-apps/plugin-sql";
export class TaskDatabaseClient extends SqlBase {
static TABLE_NAME = "task";
#db;
constructor(db) {
super(db);
this.#db = db;
}
static async load(path) {
const db = await Database.load(path);
return new TaskDatabaseClient(db);
}
async create(taskTitle) {
const { lastInsertId } = await this.myExecute(
`INSERT into ${TaskDatabaseClient.TABLE_NAME} (title) VALUES ($1)`,
[taskTitle]
);
return { id: lastInsertId, title: taskTitle };
}
async readAll() {
return this.mySelect(
`SELECT * FROM ${TaskDatabaseClient.TABLE_NAME} WHERE deleted = false`
);
}
async read(taskId) {
return this.mySelect(
`SELECT * FROM ${TaskDatabaseClient.TABLE_NAME} WHERE id = $1`,
[taskId]
);
}
async update(taskId, taskTitle) {
console.log(taskId, taskTitle);
await this.myExecute(
`UPDATE ${TaskDatabaseClient.TABLE_NAME} SET title = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`,
[taskTitle, taskId]
);
}
async delete(taskId) {
await this.myExecute(
`UPDATE ${TaskDatabaseClient.TABLE_NAME} SET deleted = true, updated_at = CURRENT_TIMESTAMP WHERE id = $1`,
[taskId]
);
}
}
画面の実装
画面の実装は、追加、編集、削除の機能を持つ簡単な TODO アプリです。まずはクエリを実装せずに、画面だけを実装します。
それぞれの機能は以下のように実装します。
import { useState } from "react";
import { Stack, HStack, Input, VStack, Text } from "@chakra-ui/react";
import { Button } from "./components/ui/Button";
function App() {
const [tasks, setTasks] = useState([]);
const [taskInput, setTaskInput] = useState("");
const [editInput, setEditInput] = useState("");
const [editIndex, setEditIndex] = useState(null);
const handleAddTask = () => {
if (editIndex !== null) {
const updatedTasks = tasks.map((task, index) =>
index === editIndex ? taskInput : task
);
setTasks(updatedTasks);
setEditIndex(null);
} else {
setTasks([...tasks, taskInput]);
}
setTaskInput("");
};
const handleEditTask = (index) => {
setEditInput(tasks[index]);
setEditIndex(index);
};
const handleUpdateTask = () => {
console.log(editInput);
const updatedTasks = tasks.map((task, index) =>
index === editIndex ? editInput : task
);
setTasks(updatedTasks);
setEditInput("");
setEditIndex(null);
};
const handleDeleteTask = (index) => {
const updatedTasks = tasks.filter((_, i) => i !== index);
setTasks(updatedTasks);
};
return (
<>
<Stack spacing={4}>
{/* form for task adding */}
<HStack>
<Input
placeholder="Input Task"
value={taskInput}
onChange={(e) => setTaskInput(e.target.value)}
/>
<Button onClick={handleAddTask}>Add</Button>
</HStack>
{/* task list */}
<VStack spacing={2} align="stretch">
{tasks.map((task, index) => (
<HStack key={index} justify="space-between">
{editIndex === index ? (
<Input
value={editInput}
onChange={(e) => setEditInput(e.target.value)}
/>
) : (
<Text>{task}</Text>
)}
<HStack>
{editIndex === index ? (
<Button onClick={handleUpdateTask}>Save</Button>
) : (
<Button onClick={() => handleEditTask(index)}>Edit</Button>
)}
<Button onClick={() => handleDeleteTask(index)}>Delete</Button>
</HStack>
</HStack>
))}
</VStack>
</Stack>
</>
);
}
export default App;
画面からクエリの呼び出し
画面の実装ができたら、クエリを実装します。クエリの実装は以下のようになります。
DB 読み込み処理は、useEffect を使用して実装します。
const [db, setDb] = useState(null);
useEffect(() => {
const loadDatabase = async () => {
const db = await TaskDatabaseClient.load("sqlite:mydatabase.db");
setDb(db);
};
loadDatabase();
}, []);
useEffect(() => {
const loadTasks = async () => {
if (db) {
const tasks = await db.readAll();
setTasks(
tasks.map((task) => {
return { id: task.id, title: task.title };
})
);
}
};
loadTasks();
}, [db]);
残りの CRUD 操作は以下のように実装します。
const handleAddTask = async () => {
const res = await db.create(taskInput);
setTasks([...tasks, { id: res.id, title: taskInput }]);
setTaskInput("");
};
const handleEditTask = (index) => {
setEditInput(tasks.find((task) => task.id === index).title);
setEditIndex(index);
};
const handleUpdateTask = async () => {
await db.update(editIndex, editInput);
const updatedTasks = tasks.map((task) =>
task.id === editIndex ? { id: task.id, title: editInput } : task
);
setTasks(updatedTasks);
setEditInput("");
setEditIndex(null);
};
const handleDeleteTask = async (index) => {
await db.delete(index);
const updatedTasks = tasks.filter((task) => task.id !== index);
setTasks(updatedTasks);
};
まとめ
今回は Tauri を使って TODO アプリを作成しました。Tauri は Rust で書かれたデスクトップアプリケーションを作成するためのフレームワークですが、React と組み合わせることで多くの機能を JavaScript で実装できるため、簡単にデスクトップアプリケーションを作成できます。