useLocal / useSession

AES-encrypted reactive browser-storage wrappers: useLocal (localStorage, persists across sessions) and useSession (sessionStorage, cleared when the tab closes), plus the full-clear helpers clearLocal / clearSession. All 4 exports are auto-imported.

📌 All frontend caching must go through these utils — never use localStorage / sessionStorage / document.cookie directly (CACHE-001, blocker). Raw usage misses three things: the cachePrefix (key collisions and data bleed when multiple projects share a domain), encryption (plaintext storage), and SSR safety (top-level storage access 500s during SSR). The only exception is storage self-managed by third-party SDKs.

How It Works

  1. A custom serializer is injected on top of VueUse useLocalStorage / useSessionStorage: reads and writes transparently call aes's decryptData / encryptData, with the key from runtimeConfig.public.cacheAesKey.
  2. Keys are automatically prefixed with runtimeConfig.public.cachePrefix, isolating projects sharing one domain.
  3. In SSR the wrapper degrades to initialValue and never touches window — safe to call on the server.
  4. clearLocal / clearSession are no-ops on the server.

Configuration lives in runtimeConfig.public of nuxt.config.ts (project constants as real values, shipping with the code; a same-named NUXT_PUBLIC_* process env can still override at runtime):

cachePrefix: 'c_'                 // cache key prefix
cacheAesKey: 'your-16-byte-key'   // AES key (16 / 24 / 32 bytes)

⚠️ cacheAesKey is in runtimeConfig.public.* and ships in the client bundle — the encryption is obfuscation, not a security barrier. See Client-Side Encryption Boundary.

⚠️ cacheAesKey must be 16 / 24 / 32 bytes (counted in utf8 bytes). With an invalid length, CryptoJS encryption still produces ciphertext but decryption returns empty — writes succeed, reads always fail, with no error at all (silent data loss). The framework detects this at build time and in dev runtime.

API

FunctionSignatureNotes
useLocal<T>(key: string, initialValue: T) => RemovableRefReactive localStorage Ref; assign to write, .remove() to delete the key
clearLocal() => voidClears the entire localStorage (including keys from other projects — use with care); server no-op
useSession<T>(key: string, initialValue: T) => RemovableRefReactive sessionStorage Ref; auto-cleared when the tab closes
clearSession() => voidClears the entire sessionStorage; server no-op

Examples

useLocal write/read + raw ciphertext in localStorage

加载中...
// User preferences (long-lived → useLocal)
const prefs = useLocal('ui-prefs', { theme: 'auto', lang: 'zh-CN' })
prefs.value.theme = 'dark' // encrypted write

// Multi-step form draft (cleared on tab close → useSession)
const draft = useSession('post-draft', '')
draft.value = 'WIP content…'
draft.remove() // delete the key

// Typed nullable: assigning null deletes
const account = useLocal<AccountInfo | null>('account', null)
account.value = null

// Full cleanup on logout
function logout() {
  clearLocal()
  clearSession()
}

Lifecycle Choice (CACHE-002, warning)

This data…Use
Should survive sessions / be shared across tabs (preferences, drafts, remember-me)useLocal
One-off / single-tab handoff (multi-step form drafts, list→detail flags)useSession

Classic failures: a long-lived preference stored in useSession (gone when the tab closes); state meant to sync across tabs stored in useSession (other tabs can't see it).

Caveats

  • clearLocal() / clearSession() clear everything — they do not filter by cachePrefix (CACHE-003): internally they are raw localStorage.clear() — on a shared domain they wipe other projects' data too. Full-clear on logout is acceptable for the (typical) dedicated-domain applier; on shared domains or for module-scoped cleanup, use each Ref's .remove() instead.
  • Hydration mismatch: the server's first render returns initialValue and switches to the real client value after hydration; if the two differ and are rendered directly into the template, you may get hydration warnings — wrap in <ClientOnly> to avoid.
  • Sensitive fields (passwords / private keys) must never go in cookies (sent on every request + plainly visible in DevTools); use useLocal instead (only read on explicit JS access).

The framework overrides Nuxt's built-in useCookie via a same-name auto-import (framework#199) — writing useCookie(key, opts) in an applier gives you the framework's encrypted version: the key is automatically prefixed with cachePrefix, values are transparently AES encrypted/decrypted (same security model as useLocal / useSession: obfuscation, not a security boundary), and it returns a WritableComputedRef<T | null> where assigning null deletes the cookie. maxAge / sameSite / secure / path / domain / default are passed through.

const token = useCookie<string>('token', { maxAge: 60 * 60 * 24 * 30, sameSite: 'lax', secure: true })
token.value = 'abc'   // written encrypted
token.value = null    // deletes the cookie

If you need Nuxt's native plaintext version (rare — e.g. reading a plaintext cookie written by a third party), import it explicitly: import { useCookie } from 'nuxt/app'.