ReactからSQLite公式のsqlite3 WASMを使ってみた

React v18から [sqlite3 WASM](https://sqlite.org/wasm/doc/trunk/index.md)（SQLite公式のWebAssembly実装）を使ってみました。
- 外部のWeb APIからデータを取得するのに、[JSONPlaceholder](https://jsonplaceholder.typicode.com/) の [todos API](https://jsonplaceholder.typicode.com/todos) を使用します。
そのデータ内容は下図の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/4b7e5b6d-e0bd-ddd4-d170-26a2dc6f1529.png)


# アプリ実行イメージ

先にアプリ実行イメージを示しておきます。

- まず「データ取得」ボタンを押すと、[todos API](https://jsonplaceholder.typicode.com/todos) を呼んで、レスポンスデータをsqlite3 WASMに保存します。
- データ取得後に「表示」ボタンを押すと、sqlite3 WASMにクエリーを発行して、id列とtitle列をブラウザ画面に表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327312/3b75c13d-d4e4-a970-1b0c-5618fba0df16.png)

# 参考サイト（感謝します）

- [SQLite Wasm in the browser backed by the Origin Private File System](https://developer.chrome.com/en/blog/sqlite-wasm-in-the-browser-backed-by-the-origin-private-file-system/)
- [SQLite公式のWASM実装を試す](https://lealog.hateblo.jp/entry/2022/12/02/134526)

# ライブラリのダウンロード

- 本記事執筆時点で、npmやyarnでインストールできません。
- [公式ページ](https://sqlite.org//download.html#:~:text=WebAssembly%20%26%20JavaScript) の「WebAssembly & JavaScript」からダウンロードします。
- 本記事では sqlite-wasm-3410100.zip をダウンロードし、プロジェクト配下のsrcディレクトリの下に解凍しました。

# ソースコード

## todoのデータ型

TypeScriptですので、必要な型を定義します。

```src/types/todo.ts

export type TodoType = {
    userId: number;
    id: number;
    title: string;
    completed: boolean;
};
```

## データ表示コンポーネント

「表示」ボタンが押されたときに、todoのid列と title列を表示するコンポーネントです。

```src/Todo.tsx

import { FC } from "react"
import { TodoType } from "./types/todo"

type Props = {
    todo: TodoType;
};

export const Todo: FC<Props> = (props) => {
    const { todo } = props;
    return <p>{`(${todo.id}) ${todo.title}`}</p>;
}
```

## 主たる処理を行うコンポーネント

- 最初に呼ばれる initDb()で、dbオブジェクトとテーブルを作成します。
- 「データ取得」ボタンが押された時に呼ばれる onClickGetTodos()で、外部の [todos API](https://jsonplaceholder.typicode.com/todos) を呼んで、レスポンスデータをsqlite3にINSERTします。
- 「表示」ボタンが押された時に呼ばれる onClickPrintTodos()で、sqlite3にSELECT文を発行して、結果データをブラウザ画面に表示します。

```src/App.tsx

import sqlite3InitModule from "./sqlite-wasm-3410100/jswasm/sqlite3";
import { useState, useEffect, useRef } from "react";
import axios from "axios";
import { TodoType } from "./types/todo";
import { Todo } from "./Todo";

const createTableStmt = `
CREATE TABLE todos (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    completed INT NOT NULL
) STRICT`;

const insertStmt = `
INSERT INTO todos (
    id,
    user_id,
    title,
    completed
)
VALUES (
    $id,
    $user_id,
    $title,
    $completed)`;

const selectStmt = `
SELECT
    id,
    user_id AS userId,
    title,
    completed
FROM
    todos`;

function App() {
    const db = useRef<any | null>(null);
    const [status, setMessage] = useState<string>("");
    const [todos, setTodos] = useState<Array<TodoType>>([]);
    const [disableButton, setDisableButton] = useState<boolean>(false);

    useEffect(() => {
        (async() => {
            await initDb()
        })();
    }, []);

    const initDb = async () => {
        try {
            const sqlite3 = await sqlite3InitModule();
            const oo = sqlite3.oo1;
            db.current = new oo.DB("/foo.sqlite3", "ct");
            db.current.exec(createTableStmt);
        }
        catch (err) {
            setMessage((err as Error).toString());
        }
    };

    const onClickGetTodos = async () => {
        try {
            setDisableButton(true);
            setTodos([]);

            const res = await axios.get<Array<TodoType>>("https://jsonplaceholder.typicode.com/todos");
            setMessage(`status=${res.status}`);
            res.data.map((rec) => {
                db.current.exec({
                    sql: insertStmt,
                    bind: {
                        $id: rec.id,
                        $user_id: rec.userId,
                        $title: rec.title,
                        $completed: rec.completed
                    }
                });
            });
        }
        catch (err) {
            setMessage((err as Error).toString());
        }
        finally {
            setDisableButton(false);
        }
    };

    const onClickPrintTodos = () => {
        try {
            setDisableButton(true);
    
            let resultRows: Array<TodoType> = [];
            db.current.exec({
                sql: selectStmt,
                rowMode: "object",
                resultRows: resultRows,
                callback: function(row: TodoType) {
                    // デバッグ用
                    console.log(`id=${row.id}, userId=${row.userId}, completed=${row.completed}, title=${row.title}`);
                }
            });
            setTodos(resultRows);
        }
        catch (err) {
            setMessage((err as Error).toString());
        }
        finally {
            setDisableButton(false);
        }
    };

    return (
        <>
            <button disabled={disableButton} onClick={onClickGetTodos}>データ取得</button>
            <p>{status}</p>
            <button disabled={disableButton} onClick={onClickPrintTodos}>表示</button>
            {todos.map((todo) => {
                return <Todo key={todo.id} todo={todo} />;
            })}
        </>
    );
}

export default App;
```

# 用途

ページネーションに対応していない外部APIを使う時に、一旦全データをsqlite3に保存しておいて、Reactでページネーション処理するのに使えそうかな？と思いました（まだ試していません）。