1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TauriAdvent Calendar 2024

Day 14

Tauri+ReactでTODOアプリを作る

Posted at

TauriV2 で簡単な TODO アプリを作ってみます。
今回作成したもの。
tauri_settings_reactive.gif

事前準備

  • 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

コマンド実行後ウィンドウが立ち上がります。ちなみに、ホットリロードが効いているので、コードを修正すると自動でリロードされます。

setup.png

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;

ここまで以下のような画面となります。
image.png

画面からクエリの呼び出し

画面の実装ができたら、クエリを実装します。クエリの実装は以下のようになります。
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 で実装できるため、簡単にデスクトップアプリケーションを作成できます。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?