Contents
Introduction
Persisting user preferences in localStorage from JavaScript is a common, performant, and privacy-friendly way to remember UI choices on the client side. In a WordPress context you’ll often combine client-side persistence for fast UI feedback with server-side persistence (usermeta, options, or REST) for cross-device sync. This article covers every practical detail: API fundamentals, patterns, pitfalls, performance, error handling, fallback strategies, synchronization across tabs, versioning and migrations, expiration emulation, WordPress enqueue patterns, and production-ready utility code.
When to use localStorage
- Use it for non-sensitive UI preferences: theme (light/dark), expanded/collapsed panels, page-level filters, UI density, last active tab, etc.
- Avoid it for secrets: API keys, passwords, tokens—these belong in secure cookies or server-side storage.
- Prefer server persistence when
- you need preferences to follow the user across different devices (use usermeta or an API)
- you must audit or centrally manage preferences
Basic localStorage rules and behavior
- localStorage is synchronous and keyed by string keys. Values are strings use JSON.stringify/JSON.parse to store objects.
- Storage quota is implementation-dependent (roughly 5MB per origin on most browsers).
- localStorage persists across tabs and browser restarts for the same origin.
- In private browsing some browsers restrict or disable localStorage (wrap access in try/catch).
- Changes in one tab emit a storage event in other tabs on the same origin, enabling cross-tab sync.
Essential best practices
- Namespace your keys to avoid collisions. Example: myplugin:preferences:v1:theme.
- Version your schema so you can migrate old formats: include a version number in the stored object or key.
- Always use try/catch to handle disabled storage or QuotaExceededError.
- Debounce writes if preferences update frequently (typing, sliders) to avoid blocking the main thread.
- Validate and sanitize values read from storage before using them.
- Do not store secrets.
- Provide fallback defaults for missing keys or malformed data.
- Consider privacy consent: if GDPR requires, consider consent before long-term storage for analytics-type preferences.
Simple example: save a theme preference (light/dark)
This minimal, production-oriented snippet demonstrates storing a theme choice, applying it on load, and keeping a namespaced key. All example code blocks are placed in the required EnlighterJSRAW pre tags.
// localStorage-theme.js
(function () {
var KEY = mytheme:preferences:v1:theme
var DEFAULT = light
function storageAvailable() {
try {
var test = __storage_test__
window.localStorage.setItem(test, test)
window.localStorage.removeItem(test)
return true
} catch (e) {
return false
}
}
function getTheme() {
if (!storageAvailable()) return DEFAULT
try {
var v = window.localStorage.getItem(KEY)
return v ? v : DEFAULT
} catch (e) {
return DEFAULT
}
}
function setTheme(value) {
if (!storageAvailable()) return
try {
window.localStorage.setItem(KEY, value)
} catch (e) {
// Handle QuotaExceededError or disabled storage
console.warn(localStorage set failed, e)
}
}
// Apply theme when script loads
var theme = getTheme()
document.documentElement.setAttribute(data-theme, theme)
// Expose helper for UI control (e.g., onclick of a toggle button)
window.myTheme = {
get: getTheme,
set: function (value) {
setTheme(value)
document.documentElement.setAttribute(data-theme, value)
}
}
})()
Debouncing writes
When preferences update frequently (e.g., while dragging a slider or typing), call localStorage sparingly. A simple debounce function prevents blocking the main thread on every keystroke.
// debounce.js
function debounce(fn, wait) {
var t
return function () {
var args = arguments
clearTimeout(t)
t = setTimeout(function () {
fn.apply(null, args)
}, wait)
}
}
// Usage: debouncedSave(value)
var savePreference = function(val) {
try {
localStorage.setItem(myplugin:pref:v1:filter, JSON.stringify(val))
} catch (e) {
console.warn(save failed, e)
}
}
var debouncedSave = debounce(savePreference, 300)
Cross-tab synchronization
listening to the storage event lets you mirror preferences across open tabs in real time. The event does not fire in the same tab that made the change.
// sync-across-tabs.js
window.addEventListener(storage, function (e) {
// e.key, e.newValue, e.oldValue
if (e.key === myplugin:preferences:v1:theme) {
var newTheme = e.newValue light
document.documentElement.setAttribute(data-theme, newTheme)
// optionally notify UI controls to update state
}
})
Storing objects and arrays
Always JSON.stringify on set and JSON.parse on get. Wrap parsing in try/catch and validate the shape of data.
// object-storage.js
var KEY = myplugin:preferences:v1:settings
function saveSettings(obj) {
try {
localStorage.setItem(KEY, JSON.stringify(obj))
} catch (e) {
console.warn(saveSettings failed, e)
}
}
function loadSettings() {
try {
var s = localStorage.getItem(KEY)
return s ? JSON.parse(s) : null
} catch (e) {
console.warn(loadSettings parse failed, e)
return null
}
}
Namespacing, versioning and migration
Include a version string in the key or in the stored payload. Provide an upgrade path if the structure changes.
// migration.js
var KEY_V1 = myplugin:preferences:v1:settings
var KEY_V2 = myplugin:preferences:v2:settings
function migrateV1toV2() {
try {
var raw = localStorage.getItem(KEY_V1)
if (!raw) return // nothing to migrate
var old = JSON.parse(raw)
// Example migration: move accent into theme object
var newObj = {
theme: {
mode: old.mode light,
accent: old.accent blue
},
other: old.other {}
}
localStorage.setItem(KEY_V2, JSON.stringify(newObj))
localStorage.removeItem(KEY_V1)
} catch (e) {
console.warn(migration failed, e)
}
}
// Call migrate early in boot process
migrateV1toV2()
Emulating expiration (TTL)
localStorage has no native TTL. Store a timestamp alongside the payload and check it when reading.
// ttl-wrapper.js
var KEY = myplugin:preferences:v1:cachedData
function setWithTTL(key, value, ttlSeconds) {
var payload = {
data: value,
expiresAt: Date.now() ttlSeconds 1000
}
try {
localStorage.setItem(key, JSON.stringify(payload))
} catch (e) {
console.warn(setWithTTL failed, e)
}
}
function getWithTTL(key) {
try {
var raw = localStorage.getItem(key)
if (!raw) return null
var payload = JSON.parse(raw)
if (payload.expiresAt Date.now() > payload.expiresAt) {
localStorage.removeItem(key)
return null
}
return payload.data
} catch (e) {
console.warn(getWithTTL parse failed, e)
return null
}
}
Error handling and fallback strategies
- Always guard localStorage access with try/catch.
- If localStorage is unavailable, use an in-memory fallback object for the session so UI still responds.
- Detect QuotaExceededError to inform users or free old items (namespaced cleanup routine).
- Cookie fallback is possible for tiny values, but cookies are sent with requests (bandwidth/privacy) so prefer in-memory or server fallback for larger or sensitive data.
Security and privacy considerations
- Never store credentials or secrets in localStorage.
- Dont assume data from localStorage is trustworthy—always validate before use.
- XSS is the main risk: malicious script can read localStorage. Mitigate XSS with proper output escaping, CSP, and avoiding injecting unsafe HTML.
- Be mindful of privacy laws: persistent client-side storage may require user consent depending on jurisdiction and the nature of data.
WordPress-specific integration patterns
On WordPress, you typically ship the client-side JS via wp_enqueue_script and optionally pass initial server values with wp_localize_script or wp_add_inline_script. For persistent, cross-device preferences save to usermeta (for logged-in users) or options (site-wide), or expose a REST endpoint to persist settings.
Enqueuing a script and passing initial data
Use wp_enqueue_script and wp_localize_script (or wp_add_inline_script) to provide initial values (e.g., default or user-saved preferences) to your JS.
// functions.php (or plugin main file)
function myplugin_enqueue_scripts() {
wp_enqueue_script(
myplugin-prefs,
plugin_dir_url(__FILE__) . assets/js/localStorage-theme.js,
array(),
1.0.0,
true
)
// Pass server-side user preference for initial state (logged-in)
prefs = array(
theme => light, // calculated server-side or from get_user_meta
)
wp_localize_script(myplugin-prefs, MyPluginPrefs, prefs)
}
add_action(wp_enqueue_scripts, myplugin_enqueue_scripts)
Saving to server for logged-in users
For cross-device persistence, send preferences to the server via the REST API or admin-ajax. Save to usermeta for per-user defaults. Keep localStorage for instant UI updates and sync to server in the background.
// js: send to REST API (simplified)
function saveToServer(prefs) {
fetch(/wp-json/myplugin/v1/userprefs, {
method: POST,
credentials: same-origin,
headers: {
Content-Type: application/json,
X-WP-Nonce: MyPluginPrefs.nonce // generated via wp_create_nonce in PHP
},
body: JSON.stringify({ prefs: prefs })
}).then(function (res) {
return res.json()
}).then(function (data) {
// handle response
}).catch(function (err) {
console.warn(server save failed, err)
})
}
// PHP: register a simple REST route that uses current_user to save user meta
add_action(rest_api_init, function () {
register_rest_route(myplugin/v1, /userprefs, array(
methods => POST,
permission_callback => function () {
return is_user_logged_in()
},
callback => function (WP_REST_Request request) {
user_id = get_current_user_id()
params = request->get_json_params()
if (!isset(params[prefs])) {
return new WP_Error(no_prefs, No prefs provided, array(status => 400))
}
update_user_meta(user_id, myplugin_prefs, params[prefs])
return rest_ensure_response(array(success => true))
}
))
})
Offline sync strategy
Combine localStorage for instant local updates with background sync to server. If network is unavailable, queue changes in localStorage with a timestamp and push when online. Respect conflict resolution rules: last-write-wins or server-authoritative, depending on use case.
// offline-queue.js
var QUEUE_KEY = myplugin:queue:v1
function queueAction(action) {
try {
var q = JSON.parse(localStorage.getItem(QUEUE_KEY) [])
q.push({ action: action, ts: Date.now() })
localStorage.setItem(QUEUE_KEY, JSON.stringify(q))
} catch (e) {
console.warn(queueAction failed, e)
}
}
function flushQueue() {
var q = JSON.parse(localStorage.getItem(QUEUE_KEY) [])
if (!q.length) return
// attempt to send, then clear only upon success
fetch(/wp-json/myplugin/v1/userprefs/batch, {/.../})
.then(function (res) { / on success: / localStorage.removeItem(QUEUE_KEY) })
.catch(function () { / keep queue for later / })
}
Performance notes
- localStorage operations are synchronous avoid large writes on hot UI paths.
- Prefer batching multiple small preferences into a single JSON object write.
- Debounce or throttle frequent updates.
- Compress/shorten key names where appropriate, but keep readability for debugging.
Testing and debugging tips
- Use the browser devtools Application tab to inspect and clear localStorage keys.
- Simulate full storage by repeatedly writing until QuotaExceededError to test fallback
- Test in private/incognito modes and across browsers because behavior varies.
- Verify storage event behavior by opening multiple tabs of the same origin and making updates in one.
Complete production-ready helper module
A compact, resilient utility wrapping common patterns: namespacing, versioning, JSON serialization, TTL, and in-memory fallback when localStorage is unavailable.
// prefs-util.js
var PrefsUtil = (function () {
function _storageAvailable() {
try {
var x = __s_test__
localStorage.setItem(x, x)
localStorage.removeItem(x)
return true
} catch (e) {
return false
}
}
var hasStorage = _storageAvailable()
var memFallback = {}
function _getRaw(key) {
if (hasStorage) {
try { return localStorage.getItem(key) }
catch (e) { console.warn(localStorage.get failed, e) return null }
}
return memFallback[key] null
}
function _setRaw(key, value) {
if (hasStorage) {
try { localStorage.setItem(key, value) return true }
catch (e) { console.warn(localStorage set failed, e) return false }
}
memFallback[key] = value
return true
}
function set(key, value) {
try {
_setRaw(key, JSON.stringify(value))
} catch (e) {
console.warn(PrefsUtil.set error, e)
}
}
function get(key, defaultValue) {
try {
var raw = _getRaw(key)
if (!raw) return defaultValue
return JSON.parse(raw)
} catch (e) {
console.warn(PrefsUtil.get parse error, e)
return defaultValue
}
}
function remove(key) {
if (hasStorage) {
try { localStorage.removeItem(key) }
catch (e) { delete memFallback[key] }
} else {
delete memFallback[key]
}
}
function setWithTTL(key, value, ttlSeconds) {
set(key, { __ttl: Date.now() ttlSeconds 1000, v: value })
}
function getWithTTL(key, defaultValue) {
var o = get(key, null)
if (!o) return defaultValue
if (o.__ttl Date.now() > o.__ttl) {
remove(key)
return defaultValue
}
return o.v !== undefined ? o.v : defaultValue
}
return {
set: set,
get: get,
remove: remove,
setWithTTL: setWithTTL,
getWithTTL: getWithTTL
}
})()
Summary checklist for production
- Namespace keys and include schema versioning.
- Guard all localStorage access with try/catch and provide an in-memory fallback.
- Use JSON for objects and validate parsed data.
- Debounce frequent writes and batch smaller pieces into single writes where possible.
- Use storage event to synchronize across tabs.
- Do not store secrets sanitize data and mitigate XSS.
- For cross-device persistence, mirror local changes to the server (REST, usermeta, options) and plan conflict resolution.
- Consider privacy/consent requirements depending on your jurisdiction and data type.
References and further reading
- MDN: localStorage
- MDN: storage event
- WordPress REST API Handbook
- Enqueueing scripts/styles in WordPress
Closing notes
Using localStorage correctly gives your WordPress UI fast, resilient preference persistence. Combine good client-side practices with server-side persistence where required, and always code defensively for storage availability and data integrity. The code snippets above provide ready-to-use utilities and patterns suitable for production deployment.
|
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |
