アプリを作ってテストしていると、「さっき入力したはずのデータが消えている」「再起動すると初期化される」という場面に出くわすことがあります。Rork で開発していると特にこのあたりで詰まりやすく、原因がコードの見た目では分かりにくいことも多いです。
今回は、Rork アプリでよく起きるデータ消失・保存失敗のパターンを整理しました。自分自身のアプリ開発でぶつかった経験も交えながら、原因の特定から修正まで説明していきます。
パターン1: AsyncStorage への書き込みが await されていない
これは非常によく見かける原因です。AsyncStorage の操作は非同期なので、await を付けずに次の処理へ進んでしまうと、保存が完了する前にアプリが次のステートに移ってしまいます。
// ❌ await なし — 保存が完了する前に次の処理が走る
const saveUserName = (name: string) => {
AsyncStorage.setItem('userName', name); // 保存が終わる前にreturn
console.log('saved\!'); // この時点ではまだ保存されていない可能性がある
};
// ✅ 正しい書き方
const saveUserName = async (name: string) => {
try {
await AsyncStorage.setItem('userName', name);
console.log('saved\!'); // ここでは確実に保存済み
} catch (error) {
console.error('保存エラー:', error);
}
};Rork の AI がコードを生成する際、AsyncStorage の await を省略してしまうことがあります。生成されたコードをそのまま使う場合は、非同期処理が正しく記述されているか確認する習慣をつけておくといいかもしれません。
パターン2: useEffect の依存配列が原因でデータが上書きされる
「保存したのに、画面を再表示すると消えている」という場合、useEffect の実行タイミングが原因になっていることがあります。
const [userName, setUserName] = useState('');
// ❌ 問題のあるパターン
useEffect(() => {
// 初回だけ読み込むつもりが...
loadUserName();
}, []); // OK
useEffect(() => {
// userName が変わるたびに保存される...
// ただし、初回レンダリング時に空文字列 '' で上書きしてしまう!
AsyncStorage.setItem('userName', userName);
}, [userName]); // userName の初期値 '' で保存が走る// ✅ 修正版: isLoaded フラグで初回の空保存を防ぐ
const [userName, setUserName] = useState('');
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
const load = async () => {
const saved = await AsyncStorage.getItem('userName');
if (saved \!== null) setUserName(saved);
setIsLoaded(true); // 読み込み完了フラグを立てる
};
load();
}, []);
useEffect(() => {
if (\!isLoaded) return; // 読み込み完了前は保存しない
AsyncStorage.setItem('userName', userName);
}, [userName, isLoaded]);isLoaded フラグを使うパターンは少し冗長に見えますが、このような問題を防ぐには確実な方法です。
パターン3: キーの衝突・タイポ
「保存はされているはずなのに読み込めない」場合、AsyncStorage のキー名を間違えている可能性があります。
// ❌ タイポでキーが一致していない
await AsyncStorage.setItem('user_name', value); // 保存
const result = await AsyncStorage.getItem('userName'); // 読み込み(別のキー!)
console.log(result); // → null// ✅ キーを定数で管理する
const STORAGE_KEYS = {
USER_NAME: 'user_name',
USER_EMAIL: 'user_email',
APP_SETTINGS: 'app_settings',
} as const;
await AsyncStorage.setItem(STORAGE_KEYS.USER_NAME, value);
const result = await AsyncStorage.getItem(STORAGE_KEYS.USER_NAME); // 確実に一致キーを文字列リテラルで直書きしていると、タイポや命名揺れが起きやすくなります。定数オブジェクトに集めておくと、オートコンプリートも効いて安全です。
パターン4: JSON のシリアライズ/デシリアライズ漏れ
AsyncStorage は文字列しか保存できません。オブジェクトや配列をそのまま渡すと [object Object] という文字列で保存されてしまい、読み込み時に正しく復元できません。
// ❌ オブジェクトをそのまま保存 → "object Object" になる
const settings = { theme: 'dark', language: 'ja' };
await AsyncStorage.setItem('settings', settings); // 型エラーになる or 文字列変換される
// ✅ JSON.stringify / JSON.parse を使う
await AsyncStorage.setItem('settings', JSON.stringify(settings));
// 読み込み時
const raw = await AsyncStorage.getItem('settings');
const settings = raw ? JSON.parse(raw) : null;// ユーティリティ関数を作っておくと便利
const saveJSON = async (key: string, value: unknown) => {
await AsyncStorage.setItem(key, JSON.stringify(value));
};
const loadJSON = async <T>(key: string): Promise<T | null> => {
const raw = await AsyncStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : null;
};このユーティリティ関数を作っておくと、アプリ全体でデータの型安全性を保ちやすくなります。
パターン5: Expo Go 環境でデータが初期化される
Expo Go アプリで開発中にデータが消える場合、アプリを終了・再起動したのではなく、Expo Go の開発サーバーが再接続されたことで状態がリセットされているだけかもしれません。
実際のデータ消失かどうかは、以下の手順で確認できます。
// デバッグ用: 保存済みデータをすべてログ出力
const debugStorageKeys = async () => {
const keys = await AsyncStorage.getAllKeys();
console.log('保存済みキー:', keys);
for (const key of keys) {
const value = await AsyncStorage.getItem(key);
console.log(` ${key}:`, value);
}
};
// 開発中はボタンなどからこの関数を呼べるようにしておくExpo Go のホットリロード時は AsyncStorage の内容は保持されますが、手動でアプリを閉じて再起動した場合は読み込み処理が正しく動いているかを確認する必要があります。
パターン6: 大量データを AsyncStorage に保存しているケース
AsyncStorage はシンプルな用途には十分ですが、大量のデータ(例えば数百件のリストや画像のキャッシュ情報)を扱い始めると、書き込みが遅くなったり、まれに失敗することがあります。
この場合は MMKV や SQLite への移行を検討する価値があります。
// MMKV(mmkv パッケージ)は AsyncStorage より数十倍高速
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
// 同期API(awaitが不要)
storage.set('userName', 'Masaki');
const name = storage.getString('userName'); // 'Masaki'
// オブジェクトの場合も同様にJSON変換
storage.set('settings', JSON.stringify({ theme: 'dark' }));
const raw = storage.getString('settings');
const settings = raw ? JSON.parse(raw) : null;Rork が生成するコードでは AsyncStorage がデフォルトで使われることが多いですが、パフォーマンスが求められる場面では MMKV への切り替えを Rork の AI に指示してみてください。
詳しい使い分けについては、AsyncStorage・MMKV・SQLite の比較と選び方をご覧いただくと判断の参考になるかもしれません。
問題を素早く特定するためのデバッグ手順
- まず書き込みを確認する: 保存直後に
getItemで読み直し、値が正しいか確認する - エラーハンドリングを追加する:
try/catchで例外を捕捉してログに出す - キー名を確認する:
getAllKeys()でどんなキーが存在するか確認する - タイミングを確認する:
awaitが抜けていないか、依存配列が正しいか見直す
これらを順番に確認するだけで、多くの場合は原因を絞り込めます。
オフラインファーストの設計で事前に防ぐ
データ消失の問題は、設計段階でオフラインを意識した構造にしておくことで大幅に減らせます。AsyncStorage + SQLite を使ったオフラインファースト設計では、より堅牢なデータ永続化のパターンを紹介しています。
まずは上記のパターン1〜3を確認してみてください。特に await の抜けとキー名のタイポは、Rork で生成したコードでも起きやすいので、コードレビューのチェック項目に加えておくといいと思います。