
React業務アプリのオフライン認証:デュアルモードをシンプルに実装する4原則
業務アプリを開発していると、こんな悩みにぶつかりませんか?
「現場ではネットが繋がらないことがあるけど、認証ってどうすればいいんだろう…」
「オンライン専用にすれば楽だけど、それだと現場で使い物にならない…」
特に、工場・倉庫・建設現場・地方拠点など、ネット環境が不安定な場所で使われる業務アプリを作るときには、避けて通れないテーマだと思います。
「オンラインとオフライン、両方対応するって複雑そう…」
「コードがぐちゃぐちゃになりそうで怖い」
その気持ち、めちゃくちゃわかります。
でも、設計の勘所さえ押さえれば、思ったよりずっとシンプルに作れます。
本記事では、実際にデュアルモード認証(オンライン / オフライン両対応)を設計・実装した経験をもとに、設計の原則・実装のポイント・ハマりがちな落とし穴まで、まるっと解説していきます。
こんな方に読んでほしい
- オフライン対応の業務アプリを作る予定がある人
- 認証まわりの設計に悩んでいるエンジニア
- React + Context API での状態管理に興味がある人
- 「複雑な要件をシンプルに保つ設計」が好きな人
この記事を読み終わる頃には、「デュアルモード、意外と怖くないかも」と思えるようになっているはずです。

そもそもデュアルモード認証ってなに?なぜ必要?
デュアルモード認証とは、ざっくり言うと「オンラインでも、オフラインでも、同じアプリが動くようにする認証の仕組み」のことです。
オンライン専用じゃダメなの?
ダメなときがあります。例えばこんなシチュエーションです。
- 地下や山間部で、電波がそもそも届かない
- 通信が不安定で、認証が頻繁にタイムアウトする
- セキュリティ要件で、外部ネットワークから遮断された環境でしか使えない
こういう場所で「ネット必須です!」というアプリを渡しても、現場の人は使ってくれません。使えないアプリは、存在しないのと同じだからです。
でも、オフライン対応って実装が面倒くさいよね…
そのとおりです。だからこそ、最初の設計が9割です。あとから付け足そうとすると、本当に地獄を見ます(経験者は語る)。
設計の4原則:これさえ守れば破綻しない
私が実際に設計してみて、「これは絶対に守ったほうがいい」と思った原則が4つあります。
原則1:モードは「入口」で決める。途中で変えない
ログイン画面で、ユーザーに「オンラインで入る?オフラインで入る?」を最初に選ばせます。
セッション中に勝手に切り替わるようにしたほうが親切じゃない?という気持ちはわかりますが、やめておいたほうがいいです。
理由はシンプルで、モードを途中で切り替えると「保存先・トークン・権限」が全部リセットになるからです。リセットを丁寧にやろうとすると、状態管理が一気に複雑になります。
一度入ったモードは、ログアウトするまで変えません。このルールを守るだけで、コードがびっくりするくらいシンプルになります。
原則2:ユーザーモデルは1つに統一する
オンラインユーザーとオフラインユーザーを別の型として作りたくなりますが、やめましょう。
代わりに、User 型に mode: 'online' | 'offline' というフィールドを1つ持たせて、同じ型で扱います。
type User = {
id: string
email: string
name: string
mode: 'online' | 'offline'
permissions: string[]
// ... 共通フィールド
}
こうしておくと、画面コンポーネント側は「ユーザーがログインしている」ことだけ意識すれば良くなります。モードを気にしなきゃいけないのは、ごく一部のコードだけで済みます。
原則3:オンラインを「正」、オフラインを「縮退版」として設計する
ここが一番大事かもしれません。
2つのモードを対等に作ろうとすると、絶対に破綻します。
オンラインモードを「フル機能」と定義し、オフラインモードはその機能制限版として設計しましょう。
例えば、こんなふうに考えます。
- オンライン:全機能が使える
- オフライン:閲覧と入力はできるが、外部 API を叩く機能は使えない
「オフラインだからこの機能は使えません」を、コードではなく権限配列で表現するのがコツです。
// オフラインユーザー
{
permissions: ['basic_access', 'local_input'],
// 'external_api' は持たない
}
これなら、画面側は「権限を持っているか」だけチェックすればいいので、if (mode === 'offline') みたいな分岐が散らばらずに済みます。
原則4:状態管理は1つのコンテキストにまとめる
React で作るなら、Context + useReducer の組み合わせがめちゃくちゃ相性いいです。
オンライン用とオフライン用でコンテキストを分けたくなりますが、1つにまとめましょう。理由は原則2と同じで、画面側がモードを意識しなくて済むからです。
実装のポイント:オンラインモード編
ここからは具体的な実装の話に入っていきます。まずはオンラインモードから。
JWT + リフレッシュトークンが基本構成
オンラインモードでは、サーバーから2つのトークンを受け取ります。
- アクセストークン:API リクエストに使う(短命)
- リフレッシュトークン:アクセストークンを再発行するために使う(長命)
なぜ2つに分けるのでしょうか。セキュリティのためです。アクセストークンが漏れても短時間で無効になり、リフレッシュトークンはサーバー側で無効化(ブラックリスト化)できます。
トークンの保存先は sessionStorage を基本に
これは賛否ありますが、私のおすすめは sessionStorage を基本にして、「ログイン状態を保持する」にチェックが入ったときだけ localStorage を使うという方針です。
localStorage に入れっぱなしにすると、タブを閉じても残るので、共用 PC などで使われると地味に怖いです。基本は sessionStorage にしておくと、タブを閉じれば勝手にログアウト相当になって安全です。
状態の保存タイミングに注意
ハマりポイントがここです。
ログイン成功時、つい「dispatch してから sessionStorage に保存」と書きがちですが、逆です。
先に sessionStorage に保存してから、dispatch してください。
理由は、dispatch の直後に他のコンポーネントがマウントされて、トークンを読みに来る可能性があるからです。順番を間違えると、「ログインしたのにトークンが見つからない」という謎バグに数時間溶かします(私のことです)。
実装のポイント:オフラインモード編
続いてオフラインモードです。こちらは驚くほどシンプルです。
認証は「起動 = ログイン成功」でいい
それだけで大丈夫でしょうか。セキュリティは問題ないでしょうか。
大丈夫です。なぜなら、オフラインモードはそもそも「外部に出ない」前提だからです。端末そのものの OS ログインで守るのが基本方針になります。
具体的には、こんな感じでローカルユーザーを生成します。
const offlineUser: User = {
id: 'offline_user',
email: 'offline@local',
name: 'オフラインユーザー',
mode: 'offline',
permissions: ['basic_access'],
}
外部 API を呼ぶコードは「権限ガード」で守る
オフラインモードでうっかり外部 API を叩こうとすると、当然エラーになります。これを防ぐために、API クライアント側で権限チェックをかけておきます。
if (!user.permissions.includes('external_api')) {
return { skipped: true }
}
モードでチェックしないのがポイントです。権限でチェックすることで、原則3で書いた通り、モードによる分岐を最小限に抑えられます。
私がハマった落とし穴3つ
ここからは、実際に開発していて踏み抜いた地雷を共有します。同じ穴に落ちないでください…!
落とし穴1:タブ間でログアウトが同期されない
ブラウザで複数タブを開いて使う業務アプリだと、片方のタブでログアウトしてももう片方のタブが生きたまま、という事故が起きます。
これは、localStorage のイベントを使ってタブ間で通知し合うことで解決できます。
window.addEventListener('storage', (e) => {
if (e.key === 'logout_signal') {
// 自分もログアウト処理
}
})
落とし穴2:リフレッシュトークンが永遠に使い回される
リフレッシュトークンを使った後、古いトークンを無効化(ブラックリスト化)していますか?
していないと、漏れたトークンが永久に使い回せてしまいます。リフレッシュは必ず「ローテーション + 旧トークン無効化」をセットにしましょう。
落とし穴3:オフラインからオンラインへの「昇格」を作りたくなる
オフラインで作業したデータを、ネットが繋がったらオンラインに同期したい——その気持ち、めちゃくちゃわかります。でも、最初のバージョンでは諦めることをおすすめします。
理由は、データの競合解決が想像の10倍難しいからです。「オフラインで編集したものとオンラインの最新版、どっちを優先する?」という問いに、業務ルール込みで答える必要があります。これだけで1つのプロジェクトになります。
最初は「オフラインモードはオフラインモードで完結」と割り切るのが、現実的な落としどころです。
まとめ:シンプルに保つコツは「分岐を端に寄せる」こと
ここまで読んでいただきありがとうございました。最後に、今日の内容を振り返っておきましょう。
デュアルモード認証の設計4原則
- モードは入口で決め、途中で変えない
- ユーザーモデルは1つに統一する
- オンラインを「正」、オフラインを「縮退版」として設計する
- 状態管理は1つのコンテキストにまとめる
実装のポイント
- オンラインは JWT + リフレッシュトークン、保存は
sessionStorage基本 - オフラインは「起動 = ログイン」で OK
- モード分岐ではなく権限分岐で書く
ハマりがちな落とし穴
- タブ間ログアウト同期は最初から入れる
- リフレッシュトークンは必ずローテーション + 無効化
- オフライン→オンラインのデータ同期は最初は諦める
複雑そうに見えるデュアルモード認証ですが、「分岐を端に寄せる」「画面側にモードを意識させない」 という2つのコツさえ守れば、コードベースはシンプルなまま保てます。
オフライン対応が必要なアプリを作る予定のある方の、設計の参考になれば嬉しいです。


