plugins/lisa-expo-agy/skills/local-state/SKILL.md
This skill should be used when implementing local state management in this React Native/Expo codebase. It covers Apollo Client Reactive Variables for in-memory reactive state and React Native AsyncStorage for persistent storage. Use this skill when creating feature flags, user preferences, session state, or any client-only state that needs to survive component unmounts or app restarts.
npx skillsauth add codyswanngt/lisa local-stateInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides best practices for local state management in this React Native/Expo codebase. Local state is managed using two complementary approaches:
| 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 |
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);
import { useReactiveVar } from "@apollo/client";
const MyComponent = () => {
const preferences = useReactiveVar(userPreferencesVar);
return <Text>{preferences.theme}</Text>;
};
// Update with new object reference
userPreferencesVar({
...userPreferencesVar(),
theme: "dark",
});
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); // Update reactive variable first
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 IUserPreferences;
userPreferencesVar({ ...DEFAULT_PREFERENCES, ...parsed });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error("Failed to load preferences:", message);
}
};
Never mutate existing objects or arrays. Create new references to trigger reactivity:
// CORRECT - creates new object
userPreferencesVar({
...userPreferencesVar(),
theme: "dark",
});
// INCORRECT - mutation does NOT trigger updates
const prefs = userPreferencesVar();
prefs.theme = "dark"; // This does nothing!
userPreferencesVar(prefs); // Same reference, no update
useReactiveVar for Reactive ComponentsCalling myVar() directly does NOT trigger re-renders. Always use the hook:
// CORRECT - component re-renders when variable changes
const theme = useReactiveVar(themeVar);
// INCORRECT - no re-renders on variable change
const theme = themeVar();
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 };
};
Prefix all AsyncStorage keys with the app namespace:
// CORRECT
const STORAGE_KEY = "@whatever:filter-values";
// INCORRECT - collision risk
const STORAGE_KEY = "filters";
AsyncStorage operations can fail. Always use try/catch:
// CORRECT
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);
}
// INCORRECT - silent failures
await AsyncStorage.setItem(key, JSON.stringify(value));
AsyncStorage is unencrypted. Use expo-secure-store for sensitive data:
// CORRECT - use secure store for tokens
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("accessToken", token);
// INCORRECT - never store tokens in AsyncStorage
await AsyncStorage.setItem("accessToken", token);
Initialize persisted state early in the app lifecycle:
// In root layout or app initialization
useEffect(() => {
loadPreferences();
}, []);
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:
// stores/userPreferences.ts
import { makeVar, useReactiveVar } from "@apollo/client";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useCallback } from "react";
// Types
interface IUserPreferences {
readonly theme: "light" | "dark";
readonly language: string;
}
// Constants
const STORAGE_KEY = "@whatever:user-preferences";
const DEFAULT_PREFERENCES: IUserPreferences = {
theme: "light",
language: "en",
};
// Reactive Variable
export const userPreferencesVar =
makeVar<IUserPreferences>(DEFAULT_PREFERENCES);
// Persistence Functions
export const loadUserPreferences = async (): Promise<void> => {
try {
const stored = await AsyncStorage.getItem(STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored) as IUserPreferences;
userPreferencesVar({ ...DEFAULT_PREFERENCES, ...parsed });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error("Failed to load preferences:", message);
}
};
const saveUserPreferences = 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);
}
};
// Custom Hook
export const useUserPreferences = () => {
const preferences = useReactiveVar(userPreferencesVar);
const setTheme = useCallback((theme: "light" | "dark") => {
saveUserPreferences({ ...userPreferencesVar(), theme });
}, []);
const setLanguage = useCallback((language: string) => {
saveUserPreferences({ ...userPreferencesVar(), language });
}, []);
return { preferences, setTheme, setLanguage };
};
For comprehensive patterns and examples, see the reference files:
// WRONG - mutation
const filters = filtersVar();
filters.minAge = 25;
filtersVar(filters);
// CORRECT - new reference
filtersVar({ ...filtersVar(), minAge: 25 });
localStorage in React Native// WRONG - doesn't exist in React Native
localStorage.setItem("key", value);
// CORRECT - use AsyncStorage
await AsyncStorage.setItem("key", value);
myVar() expecting re-renders// WRONG - no reactivity
const Component = () => {
const value = myVar(); // Does NOT trigger re-renders
return <Text>{value}</Text>;
};
// CORRECT - reactive
const Component = () => {
const value = useReactiveVar(myVar);
return <Text>{value}</Text>;
};
// WRONG - AsyncStorage only accepts strings
await AsyncStorage.setItem("key", { name: "John" });
// CORRECT - serialize first
await AsyncStorage.setItem("key", JSON.stringify({ name: "John" }));
// WRONG - state resets on app restart
export const filtersVar = makeVar<Filters>(DEFAULT_FILTERS);
// CORRECT - load on app start
export const filtersVar = makeVar<Filters>(DEFAULT_FILTERS);
export const loadFilters = async () => {
/* ... */
};
// Call loadFilters() in app initialization
When writing or reviewing local state code, verify:
makeVar<Type>(default)useReactiveVar() hook, not direct myVar() calls@whatever:expo-secure-store, not AsyncStoragestores/ directorydevelopment
Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally.
development
Guidelines for upgrading Expo SDK versions and fixing dependency issues
development
Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
tools
`@expo/ui/swift-ui` package lets you use SwiftUI Views and modifiers in your app.