plugins/src/expo/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/ directorydocumentation
Onboard a user to the project via its LLM Wiki. Interviews the user about themselves in relation to the project, captures that to project-scoped memory only, then gives a guided tour of what the project is and sample questions they can ask. Use when someone is new to the project or asks to be onboarded. Read-mostly — it does not open PRs or write PII into the wiki.
documentation
Migrate an existing, hand-rolled wiki implementation onto the lisa-wiki kernel — phased and compatibility-first, with a strict no-loss guarantee. Use when adopting lisa-wiki in a repo that already has its own wiki/, ingest skills, docs, or roles. Renaming things into the canonical shape is fine; losing functionality or data is not. Ends by running /doctor.
development
Health-check the LLM Wiki. Reports orphan pages, contradictions, stale claims, broken internal links, missing index/log coverage, structure-manifest violations, and secret/tenant leaks. Use periodically or before hardening a wiki. Read-only — it reports findings, it does not fix them.
testing
Ingest source material into the LLM Wiki. With an argument (URL, file path, or prompt) it ingests that one source; with no argument it runs a full ingest across every enabled non-external-write source. Routes to the right connector, then runs the ordered pipeline (source note → synthesis → index → log → verify → state → commit/PR). Use whenever new knowledge should enter the wiki.