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.cookiedirectly (CACHE-001, blocker). Raw usage misses three things: thecachePrefix(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
- A custom serializer is injected on top of VueUse
useLocalStorage/useSessionStorage: reads and writes transparently call aes'sdecryptData/encryptData, with the key fromruntimeConfig.public.cacheAesKey. - Keys are automatically prefixed with
runtimeConfig.public.cachePrefix, isolating projects sharing one domain. - In SSR the wrapper degrades to
initialValueand never toucheswindow— safe to call on the server. clearLocal/clearSessionare 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)
⚠️
cacheAesKeyis inruntimeConfig.public.*and ships in the client bundle — the encryption is obfuscation, not a security barrier. See Client-Side Encryption Boundary.
⚠️
cacheAesKeymust 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
| Function | Signature | Notes |
|---|---|---|
useLocal | <T>(key: string, initialValue: T) => RemovableRef | Reactive localStorage Ref; assign to write, .remove() to delete the key |
clearLocal | () => void | Clears the entire localStorage (including keys from other projects — use with care); server no-op |
useSession | <T>(key: string, initialValue: T) => RemovableRef | Reactive sessionStorage Ref; auto-cleared when the tab closes |
clearSession | () => void | Clears 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 rawlocalStorage.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
initialValueand 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
useLocalinstead (only read on explicit JS access).
useCookie (encrypted cookie)
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'.