core/local-storage
Use when loading or persisting preferences with localStorage from sagas, including startup defaults and error-safe app-local helpers.
SKILL.mdskills/core/local-storage/SKILL.md
GitHublocalStorage — safe access from sagas
Use a concrete app-local helper module such as
examples/utils/safe-local-storage-saga.ts— never callwindow.localStoragedirectly in sagas or components, and do not import localStorage helpers from the package.
Source: API and Rules, examples/utils/safe-local-storage-saga.ts.
API
import {
getLocalStorageItem, // yield* call(getLocalStorageItem, key) → string | null
setLocalStorageItem, // yield* call(setLocalStorageItem, key, value)
removeLocalStorageItem, // yield* call(removeLocalStorageItem, key)
getLocalStorageKeysWithPrefix, // yield* call(getLocalStorageKeysWithPrefix, prefix) → string[]
getLocalStorageJSON, // yield* call(getLocalStorageJSON<T>, key) → T | undefined
setLocalStorageJSON, // yield* call(setLocalStorageJSON, key, value)
} from "../utils/safe-local-storage-saga";
Every helper is wrapped in try/catch at the saga layer — quota errors, SecurityError in private browsing, key enumeration failures, and JSON-parse failures are swallowed. Reads return null / undefined on failure, prefix key reads return [], and writes become no-ops.
Signatures
function* getLocalStorageItem(key: string): SagaGenerator<string | null>;
function* setLocalStorageItem(key: string, value: string): SagaGenerator<void>;
function* removeLocalStorageItem(key: string): SagaGenerator<void>;
function* getLocalStorageKeysWithPrefix(prefix: string): SagaGenerator<string[]>;
function* getLocalStorageJSON<T>(key: string): SagaGenerator<T | undefined>;
function* setLocalStorageJSON(key: string, value: unknown): SagaGenerator<void>;
Core Patterns
1. Init saga — load defaults on startup
const STORAGE_KEY = "my-feature-settings";
const defaults = { theme: "dark", fontSize: 14 };
function* loadFromLocalStorage() {
try {
const stored = yield* call(getLocalStorageItem, STORAGE_KEY);
if (stored) return { ...defaults, ...JSON.parse(stored) };
} catch {
/* Ignore parse errors */
}
return defaults;
}
export function* initSaga() {
const settings = yield* call(loadFromLocalStorage);
yield* put(loadSettings(settings));
}
The { ...defaults, ...JSON.parse(stored) } merge keeps older payloads working when new fields are added.
2. Persistence saga — watch actions, save on change
function* persistValue() {
try {
const value = yield* selectMyValue.effect();
yield* call(setLocalStorageItem, STORAGE_KEY, JSON.stringify(value));
} catch {
/* Ignore storage errors */
}
}
export function* persistenceSaga() {
yield* takeEvery([setMyValue], persistValue);
}
3. Remove a key
yield* call(removeLocalStorageItem, STORAGE_KEY);
4. Using the JSON helpers
// Read a typed value — returns undefined if missing or unparseable
const prefs = yield* call(getLocalStorageJSON<UserPrefs>, PREFS_KEY);
// Write a typed value
yield* call(setLocalStorageJSON, PREFS_KEY, nextPrefs);
5. Listing keys by prefix
const draftKeys = yield* call(getLocalStorageKeysWithPrefix, "drafts/");
for (const key of draftKeys) {
yield* call(removeLocalStorageItem, key);
}
Rules
- ✅ Always use a concrete app-local saga helper module (for example
examples/utils/safe-local-storage-saga.ts). - ✅ Keep storage keys as constants at the top of the file.
- ✅ Use
getLocalStorageKeysWithPrefix(prefix)for helper-safe key enumeration. - ❌ Never import localStorage helpers from
@themislib/themis/sagaor package utility subpaths. - ❌ Never call
window.localStoragedirectly in sagas. - ❌ Never put localStorage calls in components — they belong in sagas.
- ❌ Never assume localStorage always succeeds — it can throw on quota or in private browsing.
Common Mistakes
❌ Calling window.localStorage directly in a saga
Mechanism: Bypasses the safe wrappers that swallow quota / security errors; private-browsing users see the saga crash.
// WRONG
yield* call([window.localStorage, "setItem"], key, value);
// CORRECT
yield* call(setLocalStorageItem, key, value);
Source: Rules. Priority: HIGH.
❌ Calling localStorage from a component
Mechanism: Side effects belong in sagas; a component write cannot be replayed, logged, or tested like a saga.
// WRONG — component lifecycle/effect code writes storage directly
function persistThemeFromComponent(theme: string) {
window.localStorage.setItem("theme", theme);
}
// CORRECT — saga watching the action
yield* takeEvery(setTheme, function* () {
const theme = yield* selectTheme.effect();
yield* call(setLocalStorageItem, "theme", theme);
});
Source: Rules. Priority: HIGH.
❌ Assuming JSON.parse will never throw
Mechanism: Corrupt or legacy payloads crash the init saga and leave the app in an empty state; wrap parse in try/catch and fall through to defaults.
// WRONG
const stored = yield* call(getLocalStorageItem, KEY);
if (stored) return JSON.parse(stored);
// CORRECT
try {
const stored = yield* call(getLocalStorageItem, KEY);
if (stored) return { ...defaults, ...JSON.parse(stored) };
} catch {
/* ignore */
}
return defaults;
Source: API and Core Patterns. Priority: MEDIUM.
See also
core/sagas—takeEverypatterns and the saga lifecycle this code runs in.core/core-policy— rule that side effects live in sagas, not components.core/state-serialization— keep state JSON-serializable soJSON.stringifyround-trips cleanly.