<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>React on 怠惰技術ブログ</title>
    <link>/tags/react/</link>
    <description>Recent content in React on 怠惰技術ブログ</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 28 Mar 2026 15:00:00 +0900</lastBuildDate>
    <atom:link href="/tags/react/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計</title>
      <link>/posts/2026-03-28-save-load-system-reducer/</link>
      <pubDate>Sat, 28 Mar 2026 15:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-save-load-system-reducer/</guid>
      <description>&lt;h1 id=&#34;react-で作る堅牢なゲームセーブシステムlocalstorage-と-reducer-を疎結合に保つ設計&#34;&gt;React で作る堅牢なゲームセーブシステム：localStorage と Reducer を疎結合に保つ設計&lt;/h1&gt;
&lt;p&gt;ゲーム開発において、プレイヤーの進行状況を保存する「セーブ / ロード」は最も重要な機能の一つです。Web ブラウザで動作する React アプリケーションの場合、最も手軽な保存先は &lt;code&gt;localStorage&lt;/code&gt; です。&lt;/p&gt;
&lt;p&gt;しかし、単純に &lt;code&gt;localStorage.setItem&lt;/code&gt; をコードのあちこちに散りばめてしまうと、テスタビリティが低下し、データ構造の変更（スキーマ変更）に弱いシステムになってしまいます。&lt;/p&gt;
&lt;p&gt;本記事では、架空の RPG 『React Odyssey』を例に、&lt;code&gt;SavePort&lt;/code&gt; インターフェースと &lt;code&gt;Reducer&lt;/code&gt; の初期化関数を組み合わせた、クリーンで堅牢なセーブ / ロードの実装方法を解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ直接-localstorageを避けるのか&#34;&gt;1. 概要：なぜ「直接 localStorage」を避けるのか&lt;/h2&gt;
&lt;p&gt;React の &lt;code&gt;useReducer&lt;/code&gt; を使ったゲーム開発では、ゲームの状態（State）は一つの大きなオブジェクトとして管理されます。これを &lt;code&gt;JSON.stringify&lt;/code&gt; して &lt;code&gt;localStorage&lt;/code&gt; に保存するのは簡単です。&lt;/p&gt;
&lt;p&gt;しかし、以下の理由から、ロジックの中に直接 &lt;code&gt;localStorage&lt;/code&gt; を書くべきではありません。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;副作用の分離&lt;/strong&gt;: Reducer は純粋関数であるべきです。セーブ処理（副作用）を Reducer の中に入れることはできません。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;環境非依存&lt;/strong&gt;: 将来的に保存先を &lt;code&gt;IndexedDB&lt;/code&gt; やクラウド（Firebase 等）に変更したくなったとき、コードを大幅に書き換える必要が出てきます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;テストのしやすさ&lt;/strong&gt;: &lt;code&gt;localStorage&lt;/code&gt; が存在しない Node.js 環境（Vitest 等）でロジックのテストを行う際、モック化が容易である必要があります。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;これらを解決するために、&lt;strong&gt;Dependency Inversion Principle（依存性逆転の原則）&lt;/strong&gt; に基づいた設計を採用します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-saveport-設計抽象化の定義&#34;&gt;2. SavePort 設計：抽象化の定義&lt;/h2&gt;
&lt;p&gt;まずは、ゲームエンジン側が必要とする「セーブ機能」のインターフェースを定義します。これを &lt;code&gt;SavePort&lt;/code&gt; と呼びます。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// domain/save/save-port.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/** 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * 保存されるデータの構造（スキーマ）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * ゲームの現在の状態に加え、メタ情報を付与する
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SaveData&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;version&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt;;        &lt;span style=&#34;color:#75715e&#34;&gt;// セーブデータのバージョン
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;timestamp&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;      &lt;span style=&#34;color:#75715e&#34;&gt;// 保存日時
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;state&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;GameState&lt;/span&gt;;       &lt;span style=&#34;color:#75715e&#34;&gt;// 実際のゲーム状態
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;/**
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; * セーブ/ロードに関する抽象インターフェース
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;SavePort&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを保存する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;SaveData&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** データを読み込む。存在しない場合は null を返す */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;load&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;SaveData&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;null&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータが存在するか確認する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;exists&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;boolean&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;/** セーブデータを削除する */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;clear&lt;/span&gt;()&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;void&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;なぜインターフェースにするのか？&lt;/strong&gt;
ゲームのメインロジック（UseCase）は、この &lt;code&gt;SavePort&lt;/code&gt; を通じて読み書きを行います。この時点では「どうやって保存するか」は知らなくてよく、「保存できる」という事実だけに依存させます。&lt;/p&gt;</description>
    </item>
    <item>
      <title>React &#43; Canvas 2D API で作るターン制ローグライク：論理と描画を切り離す設計</title>
      <link>/posts/2026-03-28-react-canvas-roguelike/</link>
      <pubDate>Sat, 28 Mar 2026 13:00:00 +0900</pubDate>
      <guid>/posts/2026-03-28-react-canvas-roguelike/</guid>
      <description>&lt;h1 id=&#34;react--canvas-2d-api-で作るターン制ローグライク論理と描画を切り離す設計&#34;&gt;React + Canvas 2D API で作るターン制ローグライク：論理と描画を切り離す設計&lt;/h1&gt;
&lt;p&gt;React でゲームを作る際、多くの開発者が最初に直面するのが「DOM で描画するか、Canvas で描画するか」という選択です。特に数千枚のタイルや多数のユニットが登場するローグライクゲームでは、DOM 要素の管理はすぐにパフォーマンスの限界に達します。&lt;/p&gt;
&lt;p&gt;本稿では、React の強力な状態管理（&lt;code&gt;useReducer&lt;/code&gt;）と、Canvas 2D API の命令的な描画を組み合わせ、滑らかなアニメーションを実現しつつ堅牢なゲームロジックを維持する設計手法について解説します。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-概要なぜ-react-と-canvas-を組み合わせるのか&#34;&gt;1. 概要：なぜ React と Canvas を組み合わせるのか&lt;/h2&gt;
&lt;p&gt;React は「宣言的」な UI 構築に長けていますが、毎秒 60 回の頻度で数千の DOM 要素を更新するような動的な描画には向いていません。一方で、Canvas は「命令的」であり、ピクセル単位での高速な描画が可能ですが、状態と描画の同期を自分で行う必要があります。&lt;/p&gt;
&lt;p&gt;この二つの「いいとこ取り」をするのが、&lt;strong&gt;「ロジックは React（useReducer）で、描画は Canvas で」&lt;/strong&gt; という役割分担です。&lt;/p&gt;
&lt;h3 id=&#34;本記事で構築するアーキテクチャ&#34;&gt;本記事で構築するアーキテクチャ&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Core Logic (&lt;code&gt;useReducer&lt;/code&gt;)&lt;/strong&gt;: ゲームの「真実の状態（State of Truth）」を管理。ターン単位で離散的に変化する座標などを扱う。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Render Loop (&lt;code&gt;requestAnimationFrame&lt;/code&gt;)&lt;/strong&gt;: Canvas 上で毎フレーム実行される描画処理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interpolation (補完)&lt;/strong&gt;: 離散的な論理座標を、滑らかな描画座標へと変換するローカル状態管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-設計判断論理座標と描画座標の分離&#34;&gt;2. 設計判断：論理座標と描画座標の分離&lt;/h2&gt;
&lt;p&gt;ローグライクゲームは基本的に「ターン制」です。プレイヤーが右に移動したとき、内部データ（Core State）では &lt;code&gt;x: 10&lt;/code&gt; から &lt;code&gt;x: 11&lt;/code&gt; へと一瞬で書き換わります。しかし、これをそのまま描画すると、キャラクターがワープしたように見えてしまいます。&lt;/p&gt;
&lt;p&gt;滑らかな移動（アニメーション）を実現するためには、以下の二種類の状態を明確に分ける必要があります。&lt;/p&gt;
&lt;h3 id=&#34;なぜ分離が必要か&#34;&gt;なぜ分離が必要か&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;種類&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;管理場所&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;特徴&lt;/th&gt;
          &lt;th style=&#34;text-align: left&#34;&gt;役割&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;論理座標 (Logical Position)&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;code&gt;useReducer&lt;/code&gt; (Global)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;整数（タイル単位）。&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;当たり判定、AI、クエスト進行など。&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;&lt;strong&gt;描画座標 (Visual Position)&lt;/strong&gt;&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;Canvas 内の Actor クラス (Local)&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;小数点を含むピクセル単位。&lt;/td&gt;
          &lt;td style=&#34;text-align: left&#34;&gt;滑らかな移動、揺れ、エフェクト。&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;判断理由：&lt;/strong&gt;
Core State にアニメーションの「途中経過（x: 10.2 など）」を持たせてしまうと、ゲームロジックが描画の都合に汚染されます。例えば、「まだ移動アニメーション中だから攻撃はできない」といった判定をロジック層で書く必要が出てき、コードが複雑化します。ロジック層は常に「今は（論理的に）どこにいるか」だけを知っていれば良いのです。&lt;/p&gt;</description>
    </item>
    <item>
      <title>テスト前提で設計したWebアプリのハンズオン - 読書管理アプリ その5</title>
      <link>/posts/2026-03-04-test-driven-design-readmeter-5/</link>
      <pubDate>Thu, 05 Mar 2026 01:30:00 +0900</pubDate>
      <guid>/posts/2026-03-04-test-driven-design-readmeter-5/</guid>
      <description>&lt;h2 id=&#34;はじめに&#34;&gt;はじめに&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;./part4&#34;&gt;Part4&lt;/a&gt; でカスタム例外クラスと残りのエンドポイントを実装し、APIが完成した。&lt;/p&gt;
&lt;p&gt;Part5では &lt;code&gt;src/app/page.tsx&lt;/code&gt; に簡易UIを実装する。バックエンドのロジックやテストには一切触れない。
フロントエンドからAPIを叩いて動くものを作るだけだ。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;実装方針&#34;&gt;実装方針&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;page.tsx&lt;/code&gt; をClient Componentにする。&lt;/p&gt;
&lt;p&gt;Server Componentでfetchする方法もあるが、ボタン操作のたびにstateを更新して再描画する必要がある。
今回のUIは「ボタンを押す → APIを叩く → 一覧を再取得して画面を更新する」という流れが中心なので、
&lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; で管理するClient Componentのほうがシンプルだ。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;use client&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ファイル先頭にこの1行を追加する。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;実装するui&#34;&gt;実装するUI&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本の追加フォーム（id / title / isbn）&lt;/li&gt;
&lt;li&gt;本棚（Unread / Reading / Completed のグループ表示）&lt;/li&gt;
&lt;li&gt;各本へのアクションボタン
&lt;ul&gt;
&lt;li&gt;Unread → 「読み始める」ボタン&lt;/li&gt;
&lt;li&gt;Reading → 評価入力（1〜5）＋「読了にする」ボタン&lt;/li&gt;
&lt;li&gt;全ステータス → 削除ボタン&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;型定義&#34;&gt;型定義&lt;/h2&gt;
&lt;p&gt;APIレスポンスの型を定義する。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;BookStatus&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Unread&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Reading&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Completed&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Book&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;isbn&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;BookStatus&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;rating&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;number&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;バックエンドの &lt;code&gt;ReadingStatus&lt;/code&gt; は &lt;code&gt;as const&lt;/code&gt; で定義した文字列リテラルなので、そのまま使える。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;データ取得&#34;&gt;データ取得&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-typescript&#34; data-lang=&#34;typescript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetchBooks() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/api/books&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ok&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; Error(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;取得失敗&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;Book&lt;/span&gt;[] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;await&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;res&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setBooks&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;catch&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setError&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;instanceof&lt;/span&gt; Error &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;不明なエラー&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  } &lt;span style=&#34;color:#66d9ef&#34;&gt;finally&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setLoading&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;useEffect&lt;/span&gt;(() &lt;span style=&#34;color:#f92672&#34;&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;fetchBooks&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, []);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;fetchBooks&lt;/code&gt; は追加・ステータス変更・削除の後にも呼ぶ。
サーバーの状態を正として再取得するシンプルな方針だ。
楽観的更新（Optimistic Update）は今回やらない。&lt;/p&gt;</description>
    </item>
    <item>
      <title>歴史地図アプリを雑にk3sへデプロイした</title>
      <link>/posts/2026-02-25-k3s-history-map-deploy/</link>
      <pubDate>Wed, 25 Feb 2026 22:00:00 +0900</pubDate>
      <guid>/posts/2026-02-25-k3s-history-map-deploy/</guid>
      <description>React&#43;Vite&#43;MapLibreで作った歴史地図アプリをk3sにデプロイした話。hostPathマウント、Viteの罠、ngrokとTailscaleを駆使したファイル転送など泥臭い記録。</description>
    </item>
  </channel>
</rss>
