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 🙂 |