From lisa-expo
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 claudepluginhub codyswanngt/lisa --plugin lisa-expoThis skill uses the workspace's default tool permissions.
This skill provides best practices for local state management in this React Native/Expo codebase. Local state is managed using two complementary approaches:
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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/ directory