<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Zustand · 仁才徳</title>
    <link>https://jared.lynskey.co.nz/ja/tags/zustand/</link>
    <description>ソフトウェアエンジニアリングと開発組織のリーダーシップについて。評価、採用とチームの拡大、CI/CD、DevOps モニタリング、エージェンティック AI。ソウルより。</description>
    <generator>Hugo</generator>
    <language>ja</language>
    <copyright>© 2026 仁才徳</copyright>
    <lastBuildDate>Mon, 26 Oct 2026 00:00:00 +1200</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ja/tags/zustand/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Zustand vs React Query（TanStack Query）vs AsyncStorage：どこに何を置くべきか</title>
      <link>https://jared.lynskey.co.nz/ja/posts/2026/2026-05-05-zustand-vs-react-query/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ja/posts/2026/2026-05-05-zustand-vs-react-query/</guid>
      <pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>React</category>
      <category>React Native</category>
      <category>Zustand</category>
      <category>React Query</category>
      <category>TanStack Query</category>
      <category>AsyncStorage</category>
      <category>State Management</category>
      <category>Mobile Development</category>
      <category>Web Development</category>
      <description>ZustandとReact Queryは二者択一ではありません。React Query（TanStack Query）はサーバーステートをキャッシュし、Zustandはクライアントステートを持ち、AsyncStorageは再起動をまたいで残すべきものを保存します。ReactとReact Nativeでの使い分け、ZustandとReact Queryの併用のしかた、そして3つを組み合わせてオフライン対応を実現する方法をまとめました。</description>
      <content:encoded><![CDATA[<p>ZustandとReact Queryは競合しません。なので「Zustand vs React Query」の本当の答えは、たいてい「両方」です。React Query（現在はTanStack Queryという名前で公開されています）はサーバーステートのためのものです。サーバーが所有するデータを取得し、キャッシュし、重複したリクエストをまとめ、古くなったら再取得してくれます。Zustandはクライアントステートのためのもの。どのタブが開いているか、書きかけのフォームに何が入っているかといった、アプリだけが知っている情報です。ほとんどのアプリはこの2つを併用しますし、React NativeではAsyncStorageが3つ目のピースとして、再起動をまたいで残すべきものを受け持ちます。この記事では、その境界線を私がどこに引いているかをコードつきで説明し、3つを合わせてオフラインでも動くアプリにする方法を紹介します。</p>
<p>いま開発している見積もりアプリには、絶対に譲れない要件がひとつあります。電波がなくても動くこと。見積もりは現場を訪ねてその場で作るのですが、現場は新築の建物だったり地下だったりで、電波はあてになりません。この制約のおかげで、多くのReactプロジェクトが曖昧なままにしている「どの種類のステートをどこに置くか」という問題に、正面から向き合わざるを得ませんでした。</p>
<p>よくある失敗は、すべてをひとつのストアに突っ込むことです。APIキャッシュもユーザー設定もフォームの状態も認証トークンも、全部同じReduxストアの中。でもこれらは性質もライフサイクルも違うステートで、それぞれに合った道具があります。Zustand、React Query、AsyncStorageはそれぞれちょうどひとつの問題をうまく解決してくれるので、境界線さえ引けば、アーキテクチャはだいたい勝手に片付きます。</p>

<h2 class="relative group">ステートは3種類ある
    <div id="ステートは3種類ある" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%bc%e3%83%88%e3%81%af3%e7%a8%ae%e9%a1%9e%e3%81%82%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ライブラリの話をする前に、自分が何を管理しているのかをはっきりさせておきましょう。</p>
<p>クライアントステートは、アプリのランタイムにしか存在しないデータです。UIのトグル、選択中のタブ、フォーム入力、モーダルの開閉。上流に所有者はおらず、プロセスが終われば消えます。</p>
<p>サーバーステートは事情が違います。所有者はサーバーで、アプリが持っているのはローカルコピーにすぎません。ユーザープロフィール、商品一覧、通知、フィード。手元のコピーは古くなりますし、再取得が必要ですし、今この瞬間に別のクライアントが別のバージョンを見ているかもしれません。</p>
<p>永続ステートは、再起動をまたいで残す必要があるデータです。認証トークン、オンボーディング完了フラグ、キャッシュした設定、オフライン用データ。デバイスそのものに保存されます。</p>
<p>たいていのアプリには3つとも存在します。そして、たいていの混乱は、3つをひとつの道具で扱おうとするところから始まります。</p>

<h2 class="relative group">Zustand：クライアントステート担当
    <div id="zustandクライアントステート担当" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zustand%e3%82%af%e3%83%a9%e3%82%a4%e3%82%a2%e3%83%b3%e3%83%88%e3%82%b9%e3%83%86%e3%83%bc%e3%83%88%e6%8b%85%e5%bd%93" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>Zustandは余計な儀式のない小さなステートライブラリです。プロバイダーなし、ボイラープレートなし、何重にもネストしたコンテキストラッパーもなし。ストアを作ってコンポーネントで使う、覚えることはほぼそれだけです。</p>
<p>出番になるのは、複数のコンポーネントが同じUIステートを必要とするとき（サイドバーの開閉、アクティブなフィルター、選択中のアイテム）、APIから来ないアプリレベルのステートがあるとき（テーマ、言語、起動時に読み込むフィーチャーフラグ）、あるいはカートの計算やマルチステップのウィザードのような、本物のクライアントサイドロジックがあるときです。同期的で予測どおりに更新したいものは、だいたいここに入ります。</p>
<p>基本のストアはこんな形です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">AppState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">sidebarOpen</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">selectedFilters</span>: <span class="kt">string</span><span class="p">[]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setTheme</span><span class="o">:</span> <span class="p">(</span><span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">toggleSidebar</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setFilters</span><span class="o">:</span> <span class="p">(</span><span class="nx">filters</span>: <span class="kt">string</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useAppStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">AppState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">sidebarOpen</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">selectedFilters</span><span class="o">:</span> <span class="p">[],</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setTheme</span><span class="o">:</span> <span class="p">(</span><span class="nx">theme</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">theme</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">toggleSidebar</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="nx">sidebarOpen</span><span class="o">:</span> <span class="o">!</span><span class="nx">state</span><span class="p">.</span><span class="nx">sidebarOpen</span> <span class="p">})),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setFilters</span><span class="o">:</span> <span class="p">(</span><span class="nx">filters</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">selectedFilters</span>: <span class="kt">filters</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span></span></span></code></pre></div></div>
<p>コンポーネント側はこうなります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">Sidebar() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">sidebarOpen</span><span class="p">,</span> <span class="nx">toggleSidebar</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useAppStore</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">sidebarOpen</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="o">=</span><span class="s">&#34;sidebar&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">toggleSidebar</span><span class="p">}&gt;</span><span class="nx">Close</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="cm">/* sidebar content */</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">Zustandに入れないもの
    <div id="zustandに入れないもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#zustand%e3%81%ab%e5%85%a5%e3%82%8c%e3%81%aa%e3%81%84%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>まずAPIレスポンス。APIコールのたびにZustandストアへ書き込み、コンポーネントはクエリの代わりにストアを読む、というプロジェクトを見たことがあります。行き着く先は、キャッシュ無効化・ローディング状態・エラーハンドリング・再取得・ページネーションの手作り再実装です。それこそReact Queryが肩代わりしてくれる仕事なんですよね。</p>
<p>もうひとつは、再起動後も残したいデータ。Zustandのステートはメモリ上にあるので、アプリを閉じれば消えます。persistミドルウェアでAsyncStorageに橋渡しすることはできますが（後述します）、それは意識的にやる判断であって、癖でやることではありません。</p>

<h2 class="relative group">React Query：サーバーステート担当
    <div id="react-queryサーバーステート担当" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#react-query%e3%82%b5%e3%83%bc%e3%83%90%e3%83%bc%e3%82%b9%e3%83%86%e3%83%bc%e3%83%88%e6%8b%85%e5%bd%93" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>React Query（今はTanStack Queryですが）は、リモートデータのライフサイクル全体を面倒見てくれます。取得、キャッシュ、同期、更新、ガベージコレクション。発想の転換ポイントは、サーバーのデータを「自分が所有するステート」ではなく「新鮮に保つべきキャッシュ」として扱うことです。所有者はサーバーで、こちらはコピーを預かっているだけです。</p>
<p>私はAPI経由のデータには全部これを使っています。同じエンドポイントを複数コンポーネントが参照するデータ（リクエストは自動で重複排除されます）、ページネーションや無限スクロール、アプリに戻ってきたときにバックグラウンドで再取得したいデータ、UIを即座に変えてサーバーに拒否されたら巻き戻す楽観的更新。ぜんぶ守備範囲です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useQuery</span><span class="p">,</span> <span class="nx">useMutation</span><span class="p">,</span> <span class="nx">useQueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useUser</span><span class="p">(</span><span class="nx">userId</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">userId</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">userId</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">staleTime</span>: <span class="kt">5</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 5分間はフレッシュとみなす
</span></span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useUpdateUser() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="nx">useQueryClient</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useMutation</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutationFn</span><span class="o">:</span> <span class="p">(</span><span class="nx">data</span><span class="o">:</span> <span class="p">{</span> <span class="nx">id</span>: <span class="kt">string</span><span class="p">;</span> <span class="nx">name</span>: <span class="kt">string</span> <span class="p">})</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/users/</span><span class="si">${</span><span class="nx">data</span><span class="p">.</span><span class="nx">id</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;PATCH&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">onSuccess</span><span class="o">:</span> <span class="p">(</span><span class="nx">_</span><span class="p">,</span> <span class="nx">variables</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">invalidateQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">variables</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>コンポーネント側です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">UserProfile</span><span class="p">({</span> <span class="nx">userId</span> <span class="p">}</span><span class="o">:</span> <span class="p">{</span> <span class="nx">userId</span>: <span class="kt">string</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">data</span>: <span class="kt">user</span><span class="p">,</span> <span class="nx">isLoading</span><span class="p">,</span> <span class="nx">error</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">useUser</span><span class="p">(</span><span class="nx">userId</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">updateUser</span> <span class="o">=</span> <span class="nx">useUpdateUser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isLoading</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">Spinner</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="k">return</span> <span class="p">&lt;</span><span class="nt">ErrorMessage</span> <span class="na">error</span><span class="o">=</span><span class="p">{</span><span class="nx">error</span><span class="p">}</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;{</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">}&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">button</span>
</span></span><span class="line"><span class="cl">        <span class="na">onClick</span><span class="o">=</span><span class="p">{()</span> <span class="o">=&gt;</span> <span class="nx">updateUser</span><span class="p">.</span><span class="nx">mutate</span><span class="p">({</span> <span class="nx">id</span>: <span class="kt">userId</span><span class="p">,</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;New Name&#39;</span> <span class="p">})}</span>
</span></span><span class="line"><span class="cl">        <span class="na">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">updateUser</span><span class="p">.</span><span class="nx">isPending</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Update</span> <span class="nx">Name</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">書かずに済むもの
    <div id="書かずに済むもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9b%b8%e3%81%8b%e3%81%9a%e3%81%ab%e6%b8%88%e3%82%80%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>サーバーデータはここに置くべきだと私が言い張る理由は、タダでついてくる仕組みの多さです。5つのコンポーネントが同じユーザーを要求しても、ネットワークリクエストは1回だけ。ウィンドウがフォーカスを取り戻したり回線が復活したりすれば再取得してくれます。古いキャッシュは勝手にガベージコレクションされます。どのクエリも <code>isLoading</code>、<code>isError</code>、<code>data</code> などを返してくれるので、ローディングやエラーのUIをいちいち手作りしなくて済みます。失敗したリクエストは指数バックオフでリトライされ、ロールバック付きの楽観的更新も組み込みパターンとして用意されています。自前で作れば丸一日仕事です。</p>

<h3 class="relative group">向いていないもの
    <div id="向いていないもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%90%91%e3%81%84%e3%81%a6%e3%81%84%e3%81%aa%e3%81%84%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>クライアントだけのステートには不向きです。サーバーを経由しないデータにReact Queryを使っても、儀式が増えるだけ。モーダルの開閉にキャッシュ無効化や再取得間隔は要りません。</p>
<p>永続化レイヤーでもありません。キャッシュはメモリ上にあるので、再起動すれば空になって再取得が走ります。キャッシュの永続化はできますし、React Nativeではやるべきです（後半のオフラインの節で扱います）。それでも、正となるデータは常にサーバー側にあります。</p>

<h2 class="relative group">AsyncStorage：再起動を生き延びるもの担当
    <div id="asyncstorage再起動を生き延びるもの担当" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#asyncstorage%e5%86%8d%e8%b5%b7%e5%8b%95%e3%82%92%e7%94%9f%e3%81%8d%e5%bb%b6%e3%81%b3%e3%82%8b%e3%82%82%e3%81%ae%e6%8b%85%e5%bd%93" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>AsyncStorageはReact Nativeのキーバリューストレージです。Webでの相当品は <code>localStorage</code>（同期）か <code>IndexedDB</code>（非同期で高機能）。考え方はどちらも同じで、デバイスに書き込むことでプロセスより長生きするデータ、ということです。</p>

<h3 class="relative group">プラットフォームごとの中身
    <div id="プラットフォームごとの中身" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%97%e3%83%a9%e3%83%83%e3%83%88%e3%83%95%e3%82%a9%e3%83%bc%e3%83%a0%e3%81%94%e3%81%a8%e3%81%ae%e4%b8%ad%e8%ba%ab" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p><code>@react-native-async-storage/async-storage</code> を使っていればAPIはひとつですが、中身はプラットフォームごとにかなり違います。</p>
<p>Androidでは <code>RKStorage</code> 経由のSQLiteで、アプリの内部ストレージディレクトリ内のデータベースに保存されます。高速で信頼でき、サンドボックス化されているので他のアプリからは触れません。ただし、下の表にあるデフォルトのサイズ上限には注意してください。</p>
<p>iOSでは、小さな値は <code>NSUserDefaults</code>、大きな値はシリアライズされたファイルに入ります。こちらもアプリのコンテナ内にサンドボックス化されています。Appleは <code>NSUserDefaults</code> にハードリミットを設けていませんが、個々の値は数百KB以下に抑えるのが常識的な線です。それを超えるようなら、そもそもWatermelonDBやRealmのような本物のデータベースを検討すべきでしょう。</p>
<p>Web（React Native WebやExpo Web）では <code>localStorage</code> にフォールバックします。上限はブラウザ次第で5〜10MB程度。Web専用のReactアプリなら <code>localStorage</code> を直接使うか、大きめのデータセットには <code>idb-keyval</code> のようなラッパー経由で <code>IndexedDB</code> を使えば十分です。</p>
<table>
	<thead>
			<tr>
					<th>プラットフォーム</th>
					<th>バックエンド</th>
					<th>サイズ上限</th>
					<th>保存場所</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Android</td>
					<td>SQLite (RKStorage)</td>
					<td>デフォルト約6 MB（変更可）</td>
					<td>アプリの内部ストレージ</td>
			</tr>
			<tr>
					<td>iOS</td>
					<td>NSUserDefaults / ファイル</td>
					<td>ハードリミットなし（値は小さく保つ）</td>
					<td>アプリのサンドボックスコンテナ</td>
			</tr>
			<tr>
					<td>Web</td>
					<td>localStorage</td>
					<td>約5〜10 MB（ブラウザ依存）</td>
					<td>ブラウザのオリジンストレージ</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">私が入れているもの
    <div id="私が入れているもの" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%a7%81%e3%81%8c%e5%85%a5%e3%82%8c%e3%81%a6%e3%81%84%e3%82%8b%e3%82%82%e3%81%ae" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>認証トークンとセッションデータ、残しておきたいユーザー設定（言語、テーマ、通知設定）、オンボーディング完了フラグ、オフライン用のキャッシュデータ。要するに「再起動を生き延びる必要のある小さなキーバリュー」で、カテゴリとしてはそれがすべてです。</p>
<p>APIはストレージとして考えられる限り素直な形をしています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 値を保存する
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">,</span> <span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 値を読み取る
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// オブジェクトを保存する（シリアライズが必要）
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;user_preferences&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;dark&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">language</span><span class="o">:</span> <span class="s1">&#39;en&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">notifications</span>: <span class="kt">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// オブジェクトを読み取る
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">prefs</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;user_preferences&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;{}&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 値を削除する
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">removeItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 全部消す（注意して使うこと）
</span></span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">clear</span><span class="p">()</span></span></span></code></pre></div></div>

<h3 class="relative group">Webでの相当品
    <div id="webでの相当品" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#web%e3%81%a7%e3%81%ae%e7%9b%b8%e5%bd%93%e5%93%81" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>Web専用のReactアプリにAsyncStorageは不要です。単純なキーバリューなら <code>localStorage</code> で足ります。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// 同期的 - メインスレッドをブロックするが、小さなデータなら問題ない
</span></span></span><span class="line"><span class="cl"><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;theme&#39;</span><span class="p">,</span> <span class="s1">&#39;dark&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">theme</span> <span class="o">=</span> <span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;theme&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 構造化データの場合
</span></span></span><span class="line"><span class="cl"><span class="nx">localStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;user&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;Jared&#39;</span><span class="p">,</span> <span class="nx">role</span><span class="o">:</span> <span class="s1">&#39;admin&#39;</span> <span class="p">}))</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">localStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;user&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;{}&#39;</span><span class="p">)</span></span></span></code></pre></div></div>
<p>大きなデータセットには <code>IndexedDB</code> を使います。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="kr">get</span><span class="p">,</span> <span class="kr">set</span><span class="p">,</span> <span class="nx">del</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;idb-keyval&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="kr">set</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">,</span> <span class="nx">hugeArray</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="kr">get</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">del</span><span class="p">(</span><span class="s1">&#39;large-dataset&#39;</span><span class="p">)</span></span></span></code></pre></div></div>

<h3 class="relative group">向いていないもの
    <div id="向いていないもの-1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%90%91%e3%81%84%e3%81%a6%e3%81%84%e3%81%aa%e3%81%84%e3%82%82%e3%81%ae-1" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>キーバリューストアであってデータベースではありません。リレーショナルデータ、数千件の配列、インデックスやクエリが必要なものは、SQLite（<code>expo-sqlite</code> 経由）、WatermelonDB、Realmの領分です。</p>
<p>セキュアストレージでもありません。root化・脱獄されたデバイスではAsyncStorageの中身は読めてしまいます。機密トークンは <code>expo-secure-store</code> か <code>react-native-keychain</code> に入れてください。</p>

<h2 class="relative group">3つの組み合わせ方
    <div id="3つの組み合わせ方" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#3%e3%81%a4%e3%81%ae%e7%b5%84%e3%81%bf%e5%90%88%e3%82%8f%e3%81%9b%e6%96%b9" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>実際のアプリでは3つを同時に使います。私がよく使うパターンをいくつか。</p>

<h3 class="relative group">認証フロー
    <div id="認証フロー" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%aa%8d%e8%a8%bc%e3%83%95%e3%83%ad%e3%83%bc" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// 1. AsyncStorage: 認証トークンを永続化する
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">saveToken</span><span class="p">(</span><span class="nx">token</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">,</span> <span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">getToken</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">string</span> <span class="err">|</span> <span class="na">null</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;auth_token&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 2. Zustand: メモリ上で認証ステートを追跡する
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">AuthState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isAuthenticated</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">token</span>: <span class="kt">string</span> <span class="o">|</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setAuth</span><span class="o">:</span> <span class="p">(</span><span class="nx">token</span>: <span class="kt">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl">  <span class="nx">clearAuth</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useAuthStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">AuthState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isAuthenticated</span>: <span class="kt">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">token</span>: <span class="kt">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setAuth</span><span class="o">:</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isAuthenticated</span>: <span class="kt">true</span><span class="p">,</span> <span class="nx">token</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">  <span class="nx">clearAuth</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isAuthenticated</span>: <span class="kt">false</span><span class="p">,</span> <span class="nx">token</span>: <span class="kt">null</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 3. React Query: トークンを使ってユーザープロフィールを取得する
</span></span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useCurrentUser() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="nx">useAuthStore</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;currentUser&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/api/me&#39;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span> <span class="nx">Authorization</span><span class="o">:</span> <span class="sb">`Bearer </span><span class="si">${</span><span class="nx">token</span><span class="si">}</span><span class="sb">`</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">}).</span><span class="nx">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()),</span>
</span></span><span class="line"><span class="cl">    <span class="nx">enabled</span><span class="o">:</span> <span class="o">!!</span><span class="nx">token</span><span class="p">,</span> <span class="c1">// トークンがあるときだけ取得する
</span></span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>起動時はこうです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// アプリの初期化
</span></span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">initializeApp() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">getToken</span><span class="p">()</span> <span class="c1">// AsyncStorageから読み取る
</span></span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">useAuthStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">setAuth</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="c1">// 高速アクセスのためにZustandに入れる
</span></span></span><span class="line"><span class="cl">    <span class="c1">// React Queryが自動的にユーザープロフィールを取得する
</span></span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>AsyncStorageがトークンを再起動越しに保持し、Zustandがどこからでも安く読めるようにし、React Queryはトークンが揃った瞬間にプロフィールを取りに行く。それぞれが得意なことをひとつずつやっています。</p>

<h3 class="relative group">再起動しても消えないテーマ設定
    <div id="再起動しても消えないテーマ設定" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%86%8d%e8%b5%b7%e5%8b%95%e3%81%97%e3%81%a6%e3%82%82%e6%b6%88%e3%81%88%e3%81%aa%e3%81%84%e3%83%86%e3%83%bc%e3%83%9e%e8%a8%ad%e5%ae%9a" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">persist</span><span class="p">,</span> <span class="nx">createJSONStorage</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand/middleware&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Zustandの永続化ミドルウェアがギャップを埋める
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useThemeStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nx">persist</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">theme</span><span class="o">:</span> <span class="s1">&#39;light&#39;</span> <span class="kr">as</span> <span class="s1">&#39;light&#39;</span> <span class="o">|</span> <span class="s1">&#39;dark&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">toggleTheme</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="kr">set</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">          <span class="nx">theme</span>: <span class="kt">state.theme</span> <span class="o">===</span> <span class="s1">&#39;light&#39;</span> <span class="o">?</span> <span class="s1">&#39;dark&#39;</span> <span class="o">:</span> <span class="s1">&#39;light&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="p">})),</span>
</span></span><span class="line"><span class="cl">    <span class="p">}),</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;theme-storage&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">storage</span>: <span class="kt">createJSONStorage</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">AsyncStorage</span><span class="p">),</span> <span class="c1">// React Native
</span></span></span><span class="line"><span class="cl">      <span class="c1">// storage: createJSONStorage(() =&gt; localStorage), // Web
</span></span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></div></div>
<p>ランタイムのステートはZustandが管理し、persistミドルウェアが裏でAsyncStorage（WebならlocalStorage）に同期します。コンポーネントは永続化レイヤーの存在を知りませんし、知る必要もありません。</p>

<h3 class="relative group">オフラインファーストな読み取り
    <div id="オフラインファーストな読み取り" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%aa%e3%83%95%e3%83%a9%e3%82%a4%e3%83%b3%e3%83%95%e3%82%a1%e3%83%bc%e3%82%b9%e3%83%88%e3%81%aa%e8%aa%ad%e3%81%bf%e5%8f%96%e3%82%8a" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useQuery</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useProducts() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useQuery</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;products&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queryFn</span>: <span class="kt">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/api/products&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// オフライン用にAsyncStorageにキャッシュする
</span></span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;cached_products&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">data</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// ネットワーク失敗 - キャッシュデータを試す
</span></span></span><span class="line"><span class="cl">        <span class="kr">const</span> <span class="nx">cached</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;cached_products&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">cached</span><span class="p">)</span> <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">cached</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="nx">error</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">staleTime</span>: <span class="kt">10</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>取得とインメモリキャッシュはReact Query、ネットワークがないときのフォールバックはAsyncStorage、という分担です。</p>

<h2 class="relative group">オフラインモードを順番に組む
    <div id="オフラインモードを順番に組む" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%aa%e3%83%95%e3%83%a9%e3%82%a4%e3%83%b3%e3%83%a2%e3%83%bc%e3%83%89%e3%82%92%e9%a0%86%e7%95%aa%e3%81%ab%e7%b5%84%e3%82%80" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>ここが私にとっての本題です。一般論としても、ユーザーは地下鉄やエレベーターや飛行機の中でアプリを開くものですが、私の場合はもっと切実で、見積もりは現場で書くものなのに、現場に電波があるとは限りません。地下でスピナーが回りっぱなしというのは、見積もりを1件落とすということです。</p>
<p>うれしいことに、この3つのライブラリだけで、重量級フレームワークに頼らず、かなりしっかりしたオフライン構成が組めます。</p>

<h3 class="relative group">ステップ1：オフラインかどうかを知る
    <div id="ステップ1オフラインかどうかを知る" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%971%e3%82%aa%e3%83%95%e3%83%a9%e3%82%a4%e3%83%b3%e3%81%8b%e3%81%a9%e3%81%86%e3%81%8b%e3%82%92%e7%9f%a5%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>まず、デバイスがネットワークの状態をアプリに伝え、アプリがそれをどこかに覚えておく必要があります。イベントは <code>@react-native-community/netinfo</code> が発行してくれるので、小さなZustandストアに入れて、どこからでも読めるようにします。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">create</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;zustand&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">NetInfo</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-community/netinfo&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">NetworkState</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isOnline</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setOnline</span><span class="o">:</span> <span class="p">(</span><span class="nx">online</span>: <span class="kt">boolean</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">useNetworkStore</span> <span class="o">=</span> <span class="nx">create</span><span class="p">&lt;</span><span class="nt">NetworkState</span><span class="p">&gt;((</span><span class="kr">set</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">isOnline</span>: <span class="kt">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setOnline</span><span class="o">:</span> <span class="p">(</span><span class="nx">online</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="kr">set</span><span class="p">({</span> <span class="nx">isOnline</span>: <span class="kt">online</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl"><span class="p">}))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// アプリ起動時に一度だけ購読する
</span></span></span><span class="line"><span class="cl"><span class="nx">NetInfo</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">useNetworkStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">setOnline</span><span class="p">(</span><span class="nx">state</span><span class="p">.</span><span class="nx">isConnected</span> <span class="o">??</span> <span class="kc">false</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>これで、どのコンポーネントでも <code>useNetworkStore((s) =&gt; s.isOnline)</code> を見て、オフラインバナーを出す、送信ボタンを無効にする、「オンラインに戻ったら同期されます」と説明する、といったことができます。</p>

<h3 class="relative group">ステップ2：React Queryにオフラインを教える
    <div id="ステップ2react-queryにオフラインを教える" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%972react-query%e3%81%ab%e3%82%aa%e3%83%95%e3%83%a9%e3%82%a4%e3%83%b3%e3%82%92%e6%95%99%e3%81%88%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>React Queryには <code>networkMode</code> という、オフライン時のクエリとミューテーションの挙動を制御する仕組みが最初から入っています。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">QueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QueryClient</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">defaultOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queries</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">networkMode</span><span class="o">:</span> <span class="s1">&#39;offlineFirst&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// キャッシュデータを即座に返し、オンライン時にバックグラウンドで再取得
</span></span></span><span class="line"><span class="cl">      <span class="nx">staleTime</span>: <span class="kt">5</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">gcTime</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// キャッシュを24時間保持
</span></span></span><span class="line"><span class="cl">      <span class="nx">retry</span><span class="o">:</span> <span class="p">(</span><span class="nx">failureCount</span><span class="p">,</span> <span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// オフラインならリトライしない - また失敗するだけ
</span></span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">useNetworkStore</span><span class="p">.</span><span class="nx">getState</span><span class="p">().</span><span class="nx">isOnline</span><span class="p">)</span> <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">failureCount</span> <span class="o">&lt;</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutations</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">networkMode</span><span class="o">:</span> <span class="s1">&#39;offlineFirst&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>3つのモードはこうなっています。</p>
<table>
	<thead>
			<tr>
					<th>モード</th>
					<th>挙動</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>online</code>（デフォルト）</td>
					<td>オンライン時だけクエリを実行し、オフライン中は一時停止する。</td>
			</tr>
			<tr>
					<td><code>always</code></td>
					<td>ネットワークの状態に関係なくクエリを実行する。失敗は <code>queryFn</code> 側で処理する。</td>
			</tr>
			<tr>
					<td><code>offlineFirst</code></td>
					<td>（キャッシュデータのために）一度だけクエリを実行し、再取得はオンラインに戻るまで一時停止する。</td>
			</tr>
	</tbody>
</table>
<p>ほとんどのモバイルアプリでは <code>offlineFirst</code> が正解です。キャッシュデータが即座に表示され、新しいデータはネットワークが許すときに届きます。</p>

<h3 class="relative group">ステップ3：クエリキャッシュを永続化する
    <div id="ステップ3クエリキャッシュを永続化する" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%973%e3%82%af%e3%82%a8%e3%83%aa%e3%82%ad%e3%83%a3%e3%83%83%e3%82%b7%e3%83%a5%e3%82%92%e6%b0%b8%e7%b6%9a%e5%8c%96%e3%81%99%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>素の状態ではキャッシュはメモリだけなので、再起動すればそこら中スピナーだらけになります。オフラインモードでは、キャッシュをAsyncStorageに永続化します。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">QueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">PersistQueryClientProvider</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query-persist-client&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">createAsyncStoragePersister</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/query-async-storage-persister&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">QueryClient</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">defaultOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">queries</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">gcTime</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 24時間 - maxAge以上でなければならない
</span></span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">asyncStoragePersister</span> <span class="o">=</span> <span class="nx">createAsyncStoragePersister</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">storage</span>: <span class="kt">AsyncStorage</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">key</span><span class="o">:</span> <span class="s1">&#39;react-query-cache&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Appコンポーネントで
</span></span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">App() {</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">PersistQueryClientProvider</span>
</span></span><span class="line"><span class="cl">      <span class="na">client</span><span class="o">=</span><span class="p">{</span><span class="nx">queryClient</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="na">persistOptions</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">persister</span>: <span class="kt">asyncStoragePersister</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">maxAge</span>: <span class="kt">24</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">,</span> <span class="c1">// 24時間より古いデータは復元しない
</span></span></span><span class="line"><span class="cl">        <span class="nx">dehydrateOptions</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">shouldDehydrateQuery</span><span class="o">:</span> <span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 成功したクエリだけを永続化する
</span></span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">query</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">&#39;success&#39;</span>
</span></span><span class="line"><span class="cl">          <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">YourApp</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">PersistQueryClientProvider</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>これで、電波のない場所でアプリを開いても、真っ白な画面ではなく前回取得したデータがすぐに表示されます。回線が戻れば、React Queryが裏で静かに再取得してくれます。</p>

<h3 class="relative group">ステップ4：オフライン中の書き込みをキューに積む
    <div id="ステップ4オフライン中の書き込みをキューに積む" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%974%e3%82%aa%e3%83%95%e3%83%a9%e3%82%a4%e3%83%b3%e4%b8%ad%e3%81%ae%e6%9b%b8%e3%81%8d%e8%be%bc%e3%81%bf%e3%82%92%e3%82%ad%e3%83%a5%e3%83%bc%e3%81%ab%e7%a9%8d%e3%82%80" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>読み取りは簡単な方の半分です。おもしろくなるのは書き込みの方で、電波のないところで追加されたコメントや送信された注文、編集されたプロフィールは、キューに積んでおいて後で再実行しなければなりません。</p>
<p>React Queryの答えは、<code>useMutation</code> と <code>onMutate</code> での楽観的更新です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useMutation</span><span class="p">,</span> <span class="nx">useQueryClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">AsyncStorage</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-async-storage/async-storage&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">interface</span> <span class="nx">Comment</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">id</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">text</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">postId</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">createdAt</span>: <span class="kt">string</span>
</span></span><span class="line"><span class="cl">  <span class="nx">pending?</span>: <span class="kt">boolean</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">useAddComment</span><span class="p">(</span><span class="nx">postId</span>: <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">queryClient</span> <span class="o">=</span> <span class="nx">useQueryClient</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">useMutation</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">mutationFn</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">text</span>: <span class="kt">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="sb">`/api/posts/</span><span class="si">${</span><span class="nx">postId</span><span class="si">}</span><span class="sb">/comments`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">method</span><span class="o">:</span> <span class="s1">&#39;POST&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span> <span class="s1">&#39;Content-Type&#39;</span><span class="o">:</span> <span class="s1">&#39;application/json&#39;</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="nx">body</span>: <span class="kt">JSON.stringify</span><span class="p">({</span> <span class="nx">text</span> <span class="p">}),</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 楽観的更新 - コメントを即座に表示する
</span></span></span><span class="line"><span class="cl">    <span class="nx">onMutate</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">text</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">await</span> <span class="nx">queryClient</span><span class="p">.</span><span class="nx">cancelQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">previous</span> <span class="o">=</span> <span class="nx">queryClient</span><span class="p">.</span><span class="nx">getQueryData</span><span class="p">&lt;</span><span class="nt">Comment</span><span class="err">[]</span><span class="p">&gt;([</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">optimisticComment</span>: <span class="kt">Comment</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">id</span><span class="o">:</span> <span class="sb">`temp-</span><span class="si">${</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">text</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">postId</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">createdAt</span>: <span class="kt">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">toISOString</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="nx">pending</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// UIに「送信中...」のインジケーターを表示する
</span></span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">setQueryData</span><span class="p">&lt;</span><span class="nt">Comment</span><span class="err">[]</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="nx">old</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">[...(</span><span class="nx">old</span> <span class="o">??</span> <span class="p">[]),</span> <span class="nx">optimisticComment</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="p">{</span> <span class="nx">previous</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// 失敗したらロールバック
</span></span></span><span class="line"><span class="cl">    <span class="nx">onError</span><span class="o">:</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">text</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">context</span><span class="o">?</span><span class="p">.</span><span class="nx">previous</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">queryClient</span><span class="p">.</span><span class="nx">setQueryData</span><span class="p">([</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">],</span> <span class="nx">context</span><span class="p">.</span><span class="nx">previous</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// サーバーから実際のデータを取得するために再取得する
</span></span></span><span class="line"><span class="cl">    <span class="nx">onSettled</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">queryClient</span><span class="p">.</span><span class="nx">invalidateQueries</span><span class="p">({</span> <span class="nx">queryKey</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;comments&#39;</span><span class="p">,</span> <span class="nx">postId</span><span class="p">]</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>ミューテーションに <code>networkMode: 'offlineFirst'</code> を設定しておくと、回線が戻るまで <code>mutationFn</code> は一時停止します。楽観的更新でコメントは即座に画面に出て、実際のAPIコールは再接続時に飛びます。</p>
<p>もっと本格的なキューが必要な場合、たとえば順番どおりに再実行すべき保留変更が何十件もあるようなときは、ミューテーションキュー自体も永続化できます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">MutationCache</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 保留中のミューテーションをAsyncStorageに保存する
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">mutationCache</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MutationCache</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">onError</span>: <span class="kt">async</span> <span class="p">(</span><span class="nx">error</span><span class="p">,</span> <span class="nx">variables</span><span class="p">,</span> <span class="nx">context</span><span class="p">,</span> <span class="nx">mutation</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// デバッグのために失敗したミューテーションを記録する
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">pending</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">getItem</span><span class="p">(</span><span class="s1">&#39;pending_mutations&#39;</span><span class="p">)</span> <span class="o">??</span> <span class="s1">&#39;[]&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">pending</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">      <span class="nx">key</span>: <span class="kt">mutation.options.mutationKey</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">variables</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">timestamp</span>: <span class="kt">Date.now</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="nx">AsyncStorage</span><span class="p">.</span><span class="nx">setItem</span><span class="p">(</span><span class="s1">&#39;pending_mutations&#39;</span><span class="p">,</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">pending</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>

<h3 class="relative group">ステップ5：回線が戻ったら整合させる
    <div id="ステップ5回線が戻ったら整合させる" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%975%e5%9b%9e%e7%b7%9a%e3%81%8c%e6%88%bb%e3%81%a3%e3%81%9f%e3%82%89%e6%95%b4%e5%90%88%e3%81%95%e3%81%9b%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>再接続まわりの仕事は、React Queryがほとんど自分でやってくれます。一時停止していたミューテーションは再開し、古くなったクエリは再取得されます。こちらの仕事は、React QueryのマネージャーをReact Nativeのイベントにつなぐことだけです。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">NetInfo</span> <span class="kr">from</span> <span class="s1">&#39;@react-native-community/netinfo&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">onlineManager</span><span class="p">,</span> <span class="nx">focusManager</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@tanstack/react-query&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">AppState</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;react-native&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// React Queryにネットワーク状態の変化を伝える
</span></span></span><span class="line"><span class="cl"><span class="nx">onlineManager</span><span class="p">.</span><span class="nx">setEventListener</span><span class="p">((</span><span class="nx">setOnline</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">NetInfo</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">((</span><span class="nx">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setOnline</span><span class="p">(</span><span class="o">!!</span><span class="nx">state</span><span class="p">.</span><span class="nx">isConnected</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// アプリがフォアグラウンドに戻ったら再取得する
</span></span></span><span class="line"><span class="cl"><span class="nx">focusManager</span><span class="p">.</span><span class="nx">setEventListener</span><span class="p">((</span><span class="nx">setFocused</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">subscription</span> <span class="o">=</span> <span class="nx">AppState</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;change&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">status</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">setFocused</span><span class="p">(</span><span class="nx">status</span> <span class="o">===</span> <span class="s1">&#39;active&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">subscription</span><span class="p">.</span><span class="nx">remove</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>
<p>アプリを1時間バックグラウンドに置いてから戻ってくれば <code>focusManager</code> が再取得を走らせ、トンネルを抜けて電波が戻れば <code>onlineManager</code> が止まっていたものを再開してくれます。</p>

<h3 class="relative group">ステップ6：UIでちゃんと伝える
    <div id="ステップ6uiでちゃんと伝える" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%b9%e3%83%86%e3%83%83%e3%83%976ui%e3%81%a7%e3%81%a1%e3%82%83%e3%82%93%e3%81%a8%e4%bc%9d%e3%81%88%e3%82%8b" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>何をするにしても、ネットワークエラーを黙って握りつぶすのだけはやめましょう。オフラインであること、どの変更が保留中なのかをユーザーに見せます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-tsx" data-lang="tsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useNetworkStore</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./stores/network&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">OfflineBanner() {</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">isOnline</span> <span class="o">=</span> <span class="nx">useNetworkStore</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">isOnline</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isOnline</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">View</span> <span class="na">style</span><span class="o">=</span><span class="p">{</span><span class="nx">styles</span><span class="p">.</span><span class="nx">banner</span><span class="p">}&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Text</span><span class="p">&gt;</span><span class="nx">You</span><span class="err">&#39;</span><span class="nx">re</span> <span class="nx">offline</span><span class="p">.</span> <span class="nx">Changes</span> <span class="nx">will</span> <span class="nx">sync</span> <span class="nx">when</span> <span class="nx">you</span> <span class="nx">reconnect</span><span class="p">.&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">View</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">CommentItem</span><span class="p">({</span> <span class="nx">comment</span> <span class="p">}</span><span class="o">:</span> <span class="p">{</span> <span class="nx">comment</span>: <span class="kt">Comment</span> <span class="p">})</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">View</span> <span class="na">style</span><span class="o">=</span><span class="p">{[</span><span class="nx">styles</span><span class="p">.</span><span class="nx">comment</span><span class="p">,</span> <span class="nx">comment</span><span class="p">.</span><span class="nx">pending</span> <span class="o">&amp;&amp;</span> <span class="nx">styles</span><span class="p">.</span><span class="nx">pending</span><span class="p">]}&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Text</span><span class="p">&gt;{</span><span class="nx">comment</span><span class="p">.</span><span class="nx">text</span><span class="p">}&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span><span class="nx">comment</span><span class="p">.</span><span class="nx">pending</span> <span class="o">&amp;&amp;</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">Text</span> <span class="na">style</span><span class="o">=</span><span class="p">{</span><span class="nx">styles</span><span class="p">.</span><span class="nx">pendingLabel</span><span class="p">}&gt;</span><span class="nx">Sending</span><span class="p">...&lt;/</span><span class="nt">Text</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">)}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">View</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>

<h3 class="relative group">全体像
    <div id="全体像" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%a8%e4%bd%93%e5%83%8f" aria-label="アンカー">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">┌─────────────────────────────────────────────────┐
</span></span><span class="line"><span class="cl">│                   Components                     │
</span></span><span class="line"><span class="cl">│  useQuery() for reads    useMutation() for writes│
</span></span><span class="line"><span class="cl">└──────────┬──────────────────────┬────────────────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────┐        ┌─────▼──────┐
</span></span><span class="line"><span class="cl">     │ React Query │        │ React Query │
</span></span><span class="line"><span class="cl">     │   Cache     │        │  Mutation   │
</span></span><span class="line"><span class="cl">     │ (in-memory) │        │   Queue     │
</span></span><span class="line"><span class="cl">     └─────┬──────┘        └─────┬──────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────────────────────▼──────┐
</span></span><span class="line"><span class="cl">     │     AsyncStorage Persister         │
</span></span><span class="line"><span class="cl">     │  (survives app restart)            │
</span></span><span class="line"><span class="cl">     └─────┬──────────────────────┬──────┘
</span></span><span class="line"><span class="cl">           │                      │
</span></span><span class="line"><span class="cl">     ┌─────▼──────┐        ┌─────▼──────┐
</span></span><span class="line"><span class="cl">     │   Zustand   │        │   Network  │
</span></span><span class="line"><span class="cl">     │ (isOnline,  │◄───────│   NetInfo  │
</span></span><span class="line"><span class="cl">     │  UI state)  │        │            │
</span></span><span class="line"><span class="cl">     └────────────┘        └────────────┘</span></span></code></pre></div></div>
<table>
	<thead>
			<tr>
					<th>レイヤー</th>
					<th>ツール</th>
					<th>役割</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>ネットワーク検知</td>
					<td>Zustand + NetInfo</td>
					<td>オンライン/オフラインを追跡し、UIのバナー表示を切り替える</td>
			</tr>
			<tr>
					<td>データ取得</td>
					<td>React Query</td>
					<td>オンライン時は取得し、オフライン時はキャッシュを返す</td>
			</tr>
			<tr>
					<td>キャッシュの永続化</td>
					<td>React Query + AsyncStorage</td>
					<td>再起動時にキャッシュを復元する</td>
			</tr>
			<tr>
					<td>オフライン中の書き込み</td>
					<td>React Queryのミューテーション</td>
					<td>ミューテーションをキューに積み、再接続時に再実行する</td>
			</tr>
			<tr>
					<td>楽観的UI</td>
					<td>React Query <code>onMutate</code></td>
					<td>変更を即座に表示し、失敗したらロールバックする</td>
			</tr>
			<tr>
					<td>フォーカス時の同期</td>
					<td>React Query <code>focusManager</code></td>
					<td>アプリがフォアグラウンドに戻ったら古いデータを再取得する</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">本物の同期エンジンが必要になるとき
    <div id="本物の同期エンジンが必要になるとき" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%ac%e7%89%a9%e3%81%ae%e5%90%8c%e6%9c%9f%e3%82%a8%e3%83%b3%e3%82%b8%e3%83%b3%e3%81%8c%e5%bf%85%e8%a6%81%e3%81%ab%e3%81%aa%e3%82%8b%e3%81%a8%e3%81%8d" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>ここまでの構成は、サーバーのデータが正でオフラインは一時的なもの、という前提で成り立っています。競合解決込みの本当のオフラインファースト（2台のデバイスが同じドキュメントをオフラインで編集するノートアプリのような世界）が必要なら、このスタックでは足りず、専用の同期エンジンの出番です。WatermelonDBはReact Native向けに作られていて、SQLiteの上で動き、競合解決のための同期プロトコルを備えています。RealmはAtlas Device Syncと組み合わせれば、MongoDB Atlas経由の自動競合解決付きの完全なオフラインファーストデータベースになります。PowerSyncは既存のPostgresバックエンドと連携できるSQLiteベースの同期レイヤー。完全に自分で制御したければ、Expo SQLiteに自作の同期ロジックという道もあります。</p>
<p>Zustand + React Query + AsyncStorageで、モバイルアプリの8割くらいはカバーできます。残りの2割、つまり共同編集やマルチデバイス同期のようなオフライン中心のワークフローには、専用の同期データベースが必要です。</p>

<h2 class="relative group">どこに置くか迷ったら
    <div id="どこに置くか迷ったら" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%81%a9%e3%81%93%e3%81%ab%e7%bd%ae%e3%81%8f%e3%81%8b%e8%bf%b7%e3%81%a3%e3%81%9f%e3%82%89" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>私は迷ったとき、この順番で自問しています。</p>
<table>
	<thead>
			<tr>
					<th>質問</th>
					<th>当てはまるなら</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>サーバー/APIから来るデータか？</td>
					<td>React Query</td>
			</tr>
			<tr>
					<td>複数のコンポーネントで共有する、クライアントだけのUIステートか？</td>
					<td>Zustand</td>
			</tr>
			<tr>
					<td>アプリの再起動後も残す必要があるか？</td>
					<td>AsyncStorage（必要に応じてZustandのpersistも）</td>
			</tr>
			<tr>
					<td>機密情報（トークン、パスワード）か？</td>
					<td>expo-secure-store / react-native-keychain</td>
			</tr>
			<tr>
					<td>クエリが必要な大きな構造化データか？</td>
					<td>SQLite / WatermelonDB / Realm</td>
			</tr>
			<tr>
					<td>1つのコンポーネントだけで使う単純なフォーム入力か？</td>
					<td>useState</td>
			</tr>
	</tbody>
</table>

<h3 class="relative group">よくあるパターン
    <div id="よくあるパターン" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%82%88%e3%81%8f%e3%81%82%e3%82%8b%e3%83%91%e3%82%bf%e3%83%bc%e3%83%b3" aria-label="アンカー">#</a>
    </span>
    
</h3>
<table>
	<thead>
			<tr>
					<th>ステート</th>
					<th>ツール</th>
					<th>理由</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>APIレスポンスのデータ</td>
					<td>React Query</td>
					<td>キャッシュ、重複排除、再取得、ローディング状態</td>
			</tr>
			<tr>
					<td>選択中のタブ / アクティブなフィルター</td>
					<td>Zustand</td>
					<td>クライアントだけのデータで、複数のコンポーネントが参照する</td>
			</tr>
			<tr>
					<td>認証トークン</td>
					<td>AsyncStorage + Zustand</td>
					<td>再起動後も残り、メモリ上で素早く読める</td>
			</tr>
			<tr>
					<td>テーマ設定</td>
					<td>Zustand + persistミドルウェア</td>
					<td>再起動後も残すべきクライアントステート</td>
			</tr>
			<tr>
					<td>ショッピングカート</td>
					<td>Zustand + persistミドルウェア</td>
					<td>クライアント側のロジックが複雑で、再起動後も残すべき</td>
			</tr>
			<tr>
					<td>フォーム入力</td>
					<td>useState</td>
					<td>1つのコンポーネントで完結し、共有の必要がない</td>
			</tr>
			<tr>
					<td>APIから取得したユーザープロフィール</td>
					<td>React Query</td>
					<td>サーバーステートで、古くなりうる</td>
			</tr>
			<tr>
					<td>オンボーディング完了フラグ</td>
					<td>AsyncStorage</td>
					<td>永続化するだけの真偽値</td>
			</tr>
			<tr>
					<td>オフライン用にキャッシュしたフィード</td>
					<td>React Query + AsyncStorage</td>
					<td>サーバーから取得し、失敗したらキャッシュにフォールバック</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">プラットフォーム別のセットアップ
    <div id="プラットフォーム別のセットアップ" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e3%83%97%e3%83%a9%e3%83%83%e3%83%88%e3%83%95%e3%82%a9%e3%83%bc%e3%83%a0%e5%88%a5%e3%81%ae%e3%82%bb%e3%83%83%e3%83%88%e3%82%a2%e3%83%83%e3%83%97" aria-label="アンカー">#</a>
    </span>
    
</h2>

<h3 class="relative group">React Native (Android + iOS)
    <div id="react-native-android--ios" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#react-native-android--ios" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>3つまとめて入れます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install zustand @tanstack/react-query @react-native-async-storage/async-storage</span></span></code></pre></div></div>
<p>機密データ用にセキュアストレージも追加しておきます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx expo install expo-secure-store
</span></span><span class="line"><span class="cl"><span class="c1"># or</span>
</span></span><span class="line"><span class="cl">npm install react-native-keychain</span></span></code></pre></div></div>

<h3 class="relative group">Expo
    <div id="expo" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#expo" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>AsyncStorageはExpoでそのまま動きます。ネイティブリンクは不要です。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npx expo install @react-native-async-storage/async-storage</span></span></code></pre></div></div>

<h3 class="relative group">Web専用のReact
    <div id="web専用のreact" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#web%e5%b0%82%e7%94%a8%e3%81%aereact" aria-label="アンカー">#</a>
    </span>
    
</h3>
<p>AsyncStorageは飛ばして、<code>localStorage</code> か <code>IndexedDB</code> を直接使ってください。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install zustand @tanstack/react-query
</span></span><span class="line"><span class="cl"><span class="c1"># Optional for IndexedDB</span>
</span></span><span class="line"><span class="cl">npm install idb-keyval</span></span></code></pre></div></div>
<p>Zustandのpersistミドルウェアは、Webではデフォルトで <code>localStorage</code> を使ってくれます。</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="nx">persist</span><span class="p">(</span><span class="nx">storeConfig</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span><span class="o">:</span> <span class="s1">&#39;my-store&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// localStorage is the default on web  - no extra config needed
</span></span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div></div>

<h2 class="relative group">何度も見てきた失敗
    <div id="何度も見てきた失敗" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%bd%95%e5%ba%a6%e3%82%82%e8%a6%8b%e3%81%a6%e3%81%8d%e3%81%9f%e5%a4%b1%e6%95%97" aria-label="アンカー">#</a>
    </span>
    
</h2>
<p>APIデータをZustandに入れる。アクションの中に <code>setUsers(apiResponse.users)</code> があったら、一度手を止めてください。それはReact Queryの仕事で、その先に待っているのはキャッシュ無効化の下手な再発明です。</p>
<p>React Queryをクライアントステートに使う。<code>queryFn</code> がネットワークリクエストをしていないなら、道具を間違えています。</p>
<p>AsyncStorageをデータベース扱いする。1万件の配列をキーバリューストアにシリアライズし始めたら、素直にSQLiteなり本物のデータベースなりを使いましょう。</p>
<p>トークンを平文で置く。AsyncStorageはセキュアストレージではありません。認証トークン、APIキー、認証情報は <code>expo-secure-store</code> かプラットフォームのキーチェーンへ。</p>
<p>永続化を手作りする。マウント時にAsyncStorageを読んで、ステートが変わるたびに書き込んで……というコードを書いているなら、Zustandのpersistミドルウェアがハイドレーションもシリアライゼーションも含めて全部やってくれます。コードは減って、バグも減ります。</p>
<hr>
<p>サーバーデータはReact Queryへ。クライアントステートはZustandへ。再起動を生き延びるべきものはAsyncStorageへ、機密データはセキュアストレージへ。</p>
<p>私が関わってきた中で最悪のアーキテクチャは、どれも巨大なストアひとつにすべてが流れ込むものでした。良いものには、サーバーステート・クライアントステート・永続化すべきものの間にはっきりした線が引いてあります。その線を早めに引いておくと、あとの全部が楽になります。</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-05-05-zustand-vs-react-query/featured.jpg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
