| name | local-state |
| description | implementing local state… |
Local State Management
Overview
This skill provides best practices for local state management in this React Native/Expo codebase. Local state is managed using two complementary approaches:
- Apollo Client Reactive Variables - In-memory reactive state for UI synchronization
- React Native AsyncStorage - Persistent key-value storage for data that survives app restarts
When to Use Each Approach
| Use Case | Approach | Example |
|---|
| UI state that resets on refresh | Reactive Variable only | Modal open state, form drafts |
| User preferences that persist | Reactive Variable + AsyncStorage | Theme, language, notifications |
| Feature flags | Reactive Variable + AsyncStorage | Beta features, profiler toggle |
| Session-scoped data | Reactive Variable only | Current filter selections |
| Cross-component communication | Reactive Variable | Selected player ID, active tab |
| Authentication tokens | expo-secure-store | Access tokens, refresh tokens |
Quick Reference
Creating a Reactive Variable
import { makeVar } from "@apollo/client";
interface IUserPreferences {
readonly theme: "light" | "dark";
readonly notifications: boolean;
}
const DEFAULT_PREFERENCES: IUserPreferences = {
theme: "light",
notifications: true,
};
export const userPreferencesVar =
makeVar<IUserPreferences>(DEFAULT_PREFERENCES);
Reading in Components (Reactive)
import { useReactiveVar } from "@apollo/client";
const MyComponent = () => {
const preferences = useReactiveVar(userPreferencesVar);
return <Text>{preferences.theme}</Text>;
};
Updating Values (Immutably)
userPreferencesVar({
...userPreferencesVar(),
theme: "dark",
});
Persisting to AsyncStorage
import AsyncStorage from "@react-native-async-storage/async-storage";
const STORAGE_KEY = "@whatever:user-preferences";
export const savePreferences = async (
prefs: IUserPreferences
): Promise<void> => {
userPreferencesVar(prefs);
try {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error("Failed to save preferences:", message);
}
};
export const loadPreferences = async (): Promise<void> => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored) as ;
({ ..., ...parsed });
}
} (error) {
message = error ? error. : ;
.(, message);
}
};
Core Rules
1. Always Create New References
Never mutate existing objects or arrays. Create new references to trigger reactivity:
userPreferencesVar({
...userPreferencesVar(),
theme: "dark",
});
const prefs = userPreferencesVar();
prefs.theme = "dark";
userPreferencesVar(prefs);
2. Use useReactiveVar for Reactive Components
Calling myVar() directly does NOT trigger re-renders. Always use the hook:
const theme = useReactiveVar(themeVar);
const theme = themeVar();
3. Encapsulate Updates in Custom Hooks
Create custom hooks for testability and encapsulation:
export const useTheme = () => {
const preferences = useReactiveVar(userPreferencesVar);
const setTheme = useCallback((theme: "light" | "dark") => {
savePreferences({ ...userPreferencesVar(), theme });
}, []);
return { theme: preferences.theme, setTheme };
};
4. Use Namespace Prefixes for Storage Keys
Prefix all AsyncStorage keys with the app namespace:
const STORAGE_KEY = "@whatever:filter-values";
const STORAGE_KEY = "filters";
5. Always Handle AsyncStorage Errors
AsyncStorage operations can fail. Always use try/catch:
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(`Failed to save ${key}:`, message);
}
await AsyncStorage.setItem(key, JSON.stringify(value));
6. Never Store Sensitive Data in AsyncStorage
AsyncStorage is unencrypted. Use expo-secure-store for sensitive data:
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("accessToken", token);
await AsyncStorage.setItem("accessToken", token);
7. Load Persisted State on App Start
Initialize persisted state early in the app lifecycle:
useEffect(() => {
loadPreferences();
}, []);
File Organization
Organize reactive variables and persistence logic in dedicated store files:
features/
my-feature/
stores/
featureState.ts # Reactive variable + persistence logic
index.ts # Re-exports
Example store file structure:
import { makeVar, useReactiveVar } from "@apollo/client";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useCallback } from "react";
interface IUserPreferences {
readonly theme: "light" | "dark";
readonly language: string;
}
const STORAGE_KEY = "@whatever:user-preferences";
const DEFAULT_PREFERENCES: IUserPreferences = {
theme: "light",
language: "en",
};
export const userPreferencesVar =
makeVar<IUserPreferences>(DEFAULT_PREFERENCES);
export const loadUserPreferences = async (): Promise<void> => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
parsed = .(stored) ;
({ ..., ...parsed });
}
} (error) {
message = error ? error. : ;
.(, message);
}
};
saveUserPreferences = (: ): <> => {
(prefs);
{
.(, .(prefs));
} (error) {
message = error ? error. : ;
.(, message);
}
};
= () => {
preferences = (userPreferencesVar);
setTheme = ( {
({ ...(), theme });
}, []);
setLanguage = ( {
({ ...(), language });
}, []);
{ preferences, setTheme, setLanguage };
};
Detailed Reference
For comprehensive patterns and examples, see the reference files:
Anti-Patterns to Avoid
Never mutate reactive variable values
const filters = filtersVar();
filters.minAge = 25;
filtersVar(filters);
filtersVar({ ...filtersVar(), minAge: 25 });
Never use localStorage in React Native
localStorage.setItem("key", value);
await AsyncStorage.setItem("key", value);
Never call myVar() expecting re-renders
const Component = () => {
const value = myVar();
return <Text>{value}</Text>;
};
const Component = () => {
const value = useReactiveVar(myVar);
return <Text>{value}</Text>;
};
Never store without serialization
await AsyncStorage.setItem("key", { name: "John" });
await AsyncStorage.setItem("key", JSON.stringify({ name: "John" }));
Never forget to load persisted state
export const filtersVar = makeVar<Filters>(DEFAULT_FILTERS);
export const filtersVar = makeVar<Filters>(DEFAULT_FILTERS);
export const loadFilters = async () => {
};
Validation Checklist
When writing or reviewing local state code, verify: