【React × Cloudflare Workers × Hono入門】第2弾:React構築 D1データベース連携

webapp-2 プログラミング

1. はじめに

  • 今回のゴール: 前回構築したバックエンドAPI(Hono)に、Reactで作成したフロントエンドとCloudflare D1を繋ぎ、ブラウザ画面からユーザーの取得・追加を行えるようにします。
  • 全体の手順:
    1. React(Vite)プロジェクトのセットアップ
    2. D1データベースの構築
    3. Reactからの fetch API呼び出し&画面実装

前回の記事はこちらです。

2. 準備:D1データベース構築

cloudflare D1の詳細な解説は以前の記事を参照してください。ここでは必要なコマンドのみを実行していきます。

① D1データベースの作成

バックエンドが入っているディレクトリで実行します。
前回の記事の続きなら my-todo-api のディレクトリで実行します。

npx wrangler d1 create todo-db

② スキーマの適用(ローカル)

schema.sql を準備し、ローカルDBにテーブルを作成します。

テスト用として以下のようなschema.sqlをmy-todo-apiのディレクトリに配置します。

-- すでにテーブルが存在する場合は削除(再初期化用)
DROP TABLE IF EXISTS todos;

-- ユーザーテーブルの作成
CREATE TABLE IF NOT EXISTS todos (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  completed INTEGER DEFAULT 0
);

-- 動作確認用の初期データ(サンプルレコード)
INSERT INTO todos (title, completed) VALUES ('部屋の掃除をする', 0);
INSERT INTO todos (title, completed) VALUES ('Reactの勉強をする', 1);

ローカルDBにテーブルを作成します。

npx wrangler d1 execute todo-db --local --file=./schema.sql

3. React (Vite) フロントエンドの構築

これも以前の記事で詳細を解説していますので参考にしてください。
ここではコマンドのみ紹介します。

① ViteでReactプロジェクトを作成

バックエンドのプロジェクト(my-todo-api)の「ひとつ上の親ディレクトリ」で実行するのがおすすめです。

📂 おすすめのディレクトリ構造

親フォルダ(project)で実行すると、バックエンドとフロントエンドが並んで配置されるため、管理がとてもスムーズになります。

Documents/
  └── my-todo/   ★ここでコマンドを実行する!
        ├── my-todo-api/      (バックエンド:Hono + D1)
        └── my-todo-frontend/ (フロントエンド:React)

以下のコマンドでフロントエンドのプロジェクトを作成します。

npm create vite@latest my-todo-frontend -- --template react-ts
cd my-todo-frontend

② バックエンド(Hono)側のCORS設定とDB接続を確認

すでに追加済みだと思いますが、フロントエンド(http://localhost:5173 等)からAPIを叩けるよう、前回の記事で作成したHono側の src/index.ts に cors() ミドルウェアが入っていることを確認します。

また、テスト用の初期データ(インメモリ配列)の部分のコードは削除して、D1への接続用コードを追加して、冒頭部分は以下のように変更します。

import { Hono } from 'hono'
import { cors } from 'hono/cors'

// Bindings の型を定義して D1 にアクセスできるようにする
type Bindings = {
  todo_db: D1Database
}

const app = new Hono<{ Bindings: Bindings }>()

// CORS設定
app.use('/api/*', cors())

// ... APIルーティング実装

4. fetch を使った API 呼び出しと画面実装

React側の App.tsx に、データ取得(GET)とデータ追加(POST)の処理を記述します。

src/App.jsx のコード例

このコードはいきなり初心者には難しいと思うので後日解説したいと思います。

import { useEffect, useState } from 'react'

interface Todo {
  id: number
  title: string
  completed: number | boolean // SQLiteから0/1で返る場合があるため
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [title, setTitle] = useState('')

  // 1. 一覧取得 (GET)
  const fetchTodos = async () => {
    try {
      const res = await fetch('http://localhost:8787/api/todos')
      const data = await res.json()
      setTodos(data)
    } catch (error) {
      console.error('取得エラー:', error)
    }
  }

  useEffect(() => {
    fetchTodos()
  }, [])

  // 2. 新規追加 (POST)
  const handleAddTodo = async (e: React.FormEvent) => {
    e.preventDefault()
    if (!title.trim()) return

    try {
      await fetch('http://localhost:8787/api/todos', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title }),
      })
      setTitle('')
      fetchTodos()
    } catch (error) {
      console.error('追加エラー:', error)
    }
  }

  // 3. 完了状態の切り替え (PUT)
  const handleToggle = async (id: number, currentStatus: number | boolean) => {
    try {
      await fetch(`http://localhost:8787/api/todos/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ completed: !currentStatus }),
      })
      fetchTodos()
    } catch (error) {
      console.error('更新エラー:', error)
    }
  }

  // 4. 削除 (DELETE)
  const handleDelete = async (id: number) => {
    try {
      await fetch(`http://localhost:8787/api/todos/${id}`, {
        method: 'DELETE',
      })
      fetchTodos()
    } catch (error) {
      console.error('削除エラー:', error)
    }
  }

  return (
    <div style={{ maxWidth: '500px', margin: '40px auto', padding: '0 20px', fontFamily: 'sans-serif', color: '#333' }}>
      
      {/* ★タイトル部分:文字サイズを小さく (fontSize: '1.5rem')、折り返しを防ぐ (whiteSpace: 'nowrap') ★ */}
      <h1 style={{ fontSize: '1.5rem', textAlign: 'center', marginBottom: '30px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
        ToDoアプリ (React × D1)
      </h1>

      {/* 登録フォーム */}
      <form onSubmit={handleAddTodo} style={{ display: 'flex', gap: '8px', marginBottom: '25px' }}>
        <input
          type="text"
          placeholder="新しいタスクを入力..."
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          style={{ flex: 1, padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }}
        />
        <button type="submit" style={{ padding: '10px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
          追加
        </button>
      </form>

      {/* タスク一覧 */}
      <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
        {todos.map((todo) => {
          const isCompleted = Boolean(todo.completed)
          return (
            <li
              key={todo.id}
              style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-between',
                padding: '15px 10px',
                borderBottom: '1px solid #eee',
                backgroundColor: isCompleted ? '#f9f9f9' : 'white'
              }}
            >
              <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', textDecoration: isCompleted ? 'line-through' : 'none', color: isCompleted ? '#888' : '#333', flex: 1, marginRight: '10px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                <input
                  type="checkbox"
                  checked={isCompleted}
                  onChange={() => handleToggle(todo.id, isCompleted)}
                  style={{ marginRight: '12px', width: '18px', height: '18px', cursor: 'pointer' }}
                />
                <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{todo.title}</span>
              </label>
              <button 
                onClick={() => handleDelete(todo.id)} 
                style={{ color: '#dc3545', backgroundColor: 'transparent', border: 'none', cursor: 'pointer', padding: '5px 10px', fontSize: '0.9rem' }}
              >
                削除
              </button>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

export default App

5. 動作確認

  • バックエンド(Hono)を起動:
    my-todo-api ディレクトリで実行します。
npx wrangler dev
  • フロントエンド(React)を起動:
    my-todo-frontend ディレクトリで実行します。
npm run dev
  • ブラウザで確認: http://localhost:5173 にアクセスし、フォームからタスクを入力して「追加」を押すと、即座に一覧へ追加されることを確認します。
  • 以下のような画面がブラウザで見れると思います。

6. まとめ

  • Reactからの fetch 呼び出しで、簡単にCloudflare D1のデータを操作できるようになりました。
  • ポイント: Hono側でのCORS許可と、データ追加後の再取得(fetchUsers)のタイミングが重要です。
  • 次回予告: (例:Cloudflare Pagesへのデプロイ / 削除・更新機能の追加 など)

コメント