Persisting only an auth token while leaving user profile, form progress, and app state in memory creates a class of silent data loss that frustrates users and erodes retention. When the OS terminates your app — which happens routinely on low-memory devices — anything not written to storage is gone. ISO 25010:2011 reliability.recoverability requires that at least three critical data categories survive an unexpected termination. Apps that fail here force users to repeat onboarding flows, re-enter form data, and lose context — all of which correlate directly with abandonment.
Critical because in-memory-only state means any OS-triggered termination silently destroys user progress across multiple data categories with no recovery path.
Use an AppState listener to flush state to storage whenever the app moves to the background, and restore it on foreground return. Wrap both paths in try/catch so corrupted storage doesn't crash startup.
import { useEffect, useRef, useState } from 'react';
import { AppState } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
export function usePersistState<T>(key: string, initial: T) {
const [state, setState] = useState<T>(initial);
const appState = useRef(AppState.currentState);
useEffect(() => {
const sub = AppState.addEventListener('change', async (next) => {
if (next.match(/inactive|background/)) {
await AsyncStorage.setItem(key, JSON.stringify(state));
} else if (appState.current.match(/inactive|background/) && next === 'active') {
const saved = await AsyncStorage.getItem(key);
if (saved) setState(JSON.parse(saved));
}
appState.current = next;
});
return () => sub.remove();
}, [state]);
return [state, setState] as const;
}
Apply this hook to every critical data category: user profile, navigation state, and any in-progress form inputs.
ID: mobile-offline-storage.data-persistence.data-persisted-lifecycle
Severity: critical
What to look for: Examine what data is being stored and whether it persists across app restarts. Look for patterns where user data, authentication tokens, form state, or app state is saved to storage. Check app lifecycle hooks (useEffect cleanup, componentWillUnmount, AppState listeners) for save logic.
Pass criteria: Enumerate all critical data types handled by the app (user profile, auth tokens, form state, preferences). At least 3 critical data types must be persisted to storage on change and restored on app launch. Before evaluating, extract and quote the AppState listener pattern or lifecycle hook that triggers persistence.
Fail criteria: Critical data is only stored in memory. App state is lost completely when app closes or crashes. Do NOT pass when only a single data type (e.g., only auth token) is persisted while other critical data remains in-memory.
Skip (N/A) when: Never — data persistence across app lifecycle is essential.
Detail on fail: Specify which critical data is not persisted. Example: "User profile data stored only in React state. When app closes, profile is lost and user must log in again on next launch."
Remediation: Use lifecycle hooks to save critical data to persistent storage. Here's a pattern for saving state when the app goes to background:
import { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
export function usePersistState(key, initialValue) {
const [state, setState] = useState(initialValue);
const appState = useRef(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription.remove();
}, [state]);
const handleAppStateChange = async (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
// App has come to foreground — restore state
const saved = await AsyncStorage.getItem(key);
if (saved) setState(JSON.parse(saved));
} else if (nextAppState.match(/inactive|background/)) {
// App going to background — save state
await AsyncStorage.setItem(key, JSON.stringify(state));
}
appState.current = nextAppState;
};
return [state, setState];
}