Introduction
When developing apps, there’s often a need to store data. Consider a simple scenario where your application features a dark mode, and users want to save their preferred setting. Most users might be entirely content with dark mode, but occasionally, you’ll encounter someone who prefers otherwise. This situation raises the question: where should this preference be stored? One approach might be to use an API with a backend to store the setting. However, for configurations that only affect the client’s experience, it may be more practical to persist this data locally. LocalStorage is one method to achieve this. In this blog post, I’ll guide you through using LocalStorage in Vue. Furthermore, I’ll demonstrate various techniques to handle this data in an elegant and type-safe manner.
Understanding LocalStorage
LocalStorage is a web storage API that lets JavaScript websites store and access data directly in the browser indefinitely. This data remains saved across browser sessions. LocalStorage is straightforward, using a key-value store model where both the key and the value are strings.
Here’s how you can use LocalStorage:
- To store data:
localStorage.setItem('myKey', 'myValue')
- To retrieve data:
localStorage.getItem('myKey')
- To remove an item:
localStorage.removeItem('myKey')
- To clear all storage:
localStorage.clear()
Using LocalStorage for Dark Mode Settings
In Vue, you can use LocalStorage to save a user’s preference for dark mode in a component.
<template>
<button: ButtonHTMLAttributes & ReservedProps
button HTMLAttributes.class?: any
class="dark-mode-toggle" @onClick?: ((payload: MouseEvent) => void) | undefined
click="function toggleDarkMode(): void
toggleDarkMode">
{{ const isDarkMode: Ref<any, any>
isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode' }}
<span: HTMLAttributes & ReservedProps
span HTMLAttributes.class?: any
class="icon" v-html="const isDarkMode: Ref<any, any>
isDarkMode ? const moonIcon: "<svg some svg </svg>"
moonIcon : const sunIcon: "<svg some svg </svg>"
sunIcon" />
</button: ButtonHTMLAttributes & ReservedProps
button>
</template>
<script setup lang="ts">
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref, const computed: {
<T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions): ComputedRef<T>;
<T, S = T>(options: WritableComputedOptions<T, S>, debugOptions?: DebuggerOptions): WritableComputedRef<T, S>;
}
computed, const onMounted: CreateHook<any>
onMounted } from 'vue'
const const isDarkMode: Ref<any, any>
isDarkMode = ref<any>(value: any): Ref<any, any> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.parse(var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.getItem(key: string): string | null
Returns the current value associated with the given key, or null if the given key does not exist.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)getItem('darkMode') ?? 'false'))
const const styleProperties: ComputedRef<{
'--background-color': string;
'--text-color': string;
}>
styleProperties = computed<{
'--background-color': string;
'--text-color': string;
}>(getter: ComputedGetter<{
'--background-color': string;
'--text-color': string;
}>, debugOptions?: DebuggerOptions): ComputedRef<...> (+1 overload)
Takes a getter function and returns a readonly reactive ref object for the
returned value from the getter. It can also take an object with get and set
functions to create a writable ref object.computed(() => ({
'--background-color': const isDarkMode: Ref<any, any>
isDarkMode.Ref<any, any>.value: any
value ? '#333' : '#FFF',
'--text-color': const isDarkMode: Ref<any, any>
isDarkMode.Ref<any, any>.value: any
value ? '#FFF' : '#333'
}))
const const sunIcon: "<svg some svg </svg>"
sunIcon = `<svg some svg </svg>`
const const moonIcon: "<svg some svg </svg>"
moonIcon = `<svg some svg </svg>`
function function applyStyles(): void
applyStyles () {
for (const [const key: string
key, const value: string
value] of var Object: ObjectConstructor
Provides functionality common to all JavaScript objects.Object.ObjectConstructor.entries<string>(o: {
[s: string]: string;
} | ArrayLike<string>): [string, string][] (+1 overload)
Returns an array of key/values of the enumerable own properties of an objectentries(const styleProperties: ComputedRef<{
'--background-color': string;
'--text-color': string;
}>
styleProperties.ComputedRef<{ '--background-color': string; '--text-color': string; }>.value: {
'--background-color': string;
'--text-color': string;
}
value)) {
var document: Document
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/document)document.Document.documentElement: HTMLElement
Gets a reference to the root node of the document.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/documentElement)documentElement.ElementCSSInlineStyle.style: CSSStyleDeclaration
[MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLElement/style)style.CSSStyleDeclaration.setProperty(property: string, value: string | null, priority?: string): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/CSSStyleDeclaration/setProperty)setProperty(const key: string
key, const value: string
value)
}
}
function function toggleDarkMode(): void
toggleDarkMode () {
const isDarkMode: Ref<any, any>
isDarkMode.Ref<any, any>.value: any
value = !const isDarkMode: Ref<any, any>
isDarkMode.Ref<any, any>.value: any
value
var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.)
Dispatches a storage event on Window objects holding an equivalent Storage object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)setItem('darkMode', var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify(const isDarkMode: Ref<any, any>
isDarkMode.Ref<any, any>.value: any
value))
function applyStyles(): void
applyStyles()
}
// On component mount, apply the stored or default styles
function onMounted(hook: any, target?: ComponentInternalInstance | null): void
onMounted(function applyStyles(): void
applyStyles)
</script>
<style scoped>
.dark-mode-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
font-size: 16px;
color: var(--text-color);
background-color: var(--background-color);
border: 1px solid var(--text-color);
border-radius: 5px;
cursor: pointer;
}
.icon {
display: inline-block;
margin-left: 10px;
}
:root {
--background-color: #FFF;
--text-color: #333;
}
body {
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
</style>
Addressing Issues with Initial Implementation
In simple scenarios, the approach works well, but it faces several challenges in larger applications:
- Type Safety and Key Validation: Always check and handle data from LocalStorage to prevent errors.
- Decoupling from LocalStorage: Avoid direct LocalStorage interactions in your components. Instead, use a utility service or state management for better code maintenance and testing.
- Error Handling: Manage exceptions like browser restrictions or storage limits properly as LocalStorage operations can fail.
- Synchronization Across Components: Use event-driven communication or shared state to keep all components updated with changes.
- Serialization Constraints: LocalStorage stores data as strings. Serialization and deserialization can be tricky, especially with complex data types.
Solutions and Best Practices for LocalStorage
To overcome these challenges, consider these solutions:
- Type Definitions: Use TypeScript to enforce type safety and help with autocompletion.
// types/localStorageTypes.ts
export type type UserSettings = {
name: string;
}
UserSettings = {name: string
name: string}
export type type LocalStorageValues = {
darkMode: boolean;
userSettings: UserSettings;
lastLogin: Date;
}
LocalStorageValues = {
darkMode: boolean
darkMode: boolean,
userSettings: UserSettings
userSettings: type UserSettings = {
name: string;
}
UserSettings,
lastLogin: Date
lastLogin: Date,
}
export type type LocalStorageKeys = keyof LocalStorageValues
LocalStorageKeys = keyof type LocalStorageValues = {
darkMode: boolean;
userSettings: UserSettings;
lastLogin: Date;
}
LocalStorageValues
- Utility Classes: Create a utility class to manage all LocalStorage operations.
// utils/LocalStorageHandler.ts
// import { LocalStorageKeys, LocalStorageValues } from '@/types/localStorageTypes';
export class class LocalStorageHandler
LocalStorageHandler {
static LocalStorageHandler.getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null
getItem<function (type parameter) K in LocalStorageHandler.getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null
K extends type LocalStorageKeys = keyof LocalStorageValues
LocalStorageKeys>(
key: K extends LocalStorageKeys
key: function (type parameter) K in LocalStorageHandler.getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null
K
): type LocalStorageValues = {
darkMode: boolean;
userSettings: UserSettings;
lastLogin: Date;
}
LocalStorageValues[function (type parameter) K in LocalStorageHandler.getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null
K] | null {
try {
const const item: string | null
item = var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.getItem(key: string): string | null
Returns the current value associated with the given key, or null if the given key does not exist.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)getItem(key: keyof LocalStorageValues
key);
return const item: string | null
item ? var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.parse(const item: string
item) as type LocalStorageValues = {
darkMode: boolean;
userSettings: UserSettings;
lastLogin: Date;
}
LocalStorageValues[function (type parameter) K in LocalStorageHandler.getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null
K] : null;
} catch (function (local var) error: unknown
error) {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stderr` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
```
If formatting elements (e.g. `%d`) are not found in the first string then
[`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the
resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)
for more information.error(`Error retrieving item from localStorage: ${function (local var) error: unknown
error}`);
return null;
}
}
static LocalStorageHandler.setItem<K extends LocalStorageKeys>(key: K, value: LocalStorageValues[K]): void
setItem<function (type parameter) K in LocalStorageHandler.setItem<K extends LocalStorageKeys>(key: K, value: LocalStorageValues[K]): void
K extends type LocalStorageKeys = keyof LocalStorageValues
LocalStorageKeys>(
key: K extends LocalStorageKeys
key: function (type parameter) K in LocalStorageHandler.setItem<K extends LocalStorageKeys>(key: K, value: LocalStorageValues[K]): void
K,
value: LocalStorageValues[K]
value: type LocalStorageValues = {
darkMode: boolean;
userSettings: UserSettings;
lastLogin: Date;
}
LocalStorageValues[function (type parameter) K in LocalStorageHandler.setItem<K extends LocalStorageKeys>(key: K, value: LocalStorageValues[K]): void
K]
): void {
try {
const const item: string
item = var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.stringify(value: boolean | UserSettings | Date
value);
var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.)
Dispatches a storage event on Window objects holding an equivalent Storage object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)setItem(key: keyof LocalStorageValues
key, const item: string
item);
} catch (function (local var) error: unknown
error) {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stderr` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
```
If formatting elements (e.g. `%d`) are not found in the first string then
[`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the
resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)
for more information.error(`Error setting item in localStorage: ${function (local var) error: unknown
error}`);
}
}
static LocalStorageHandler.removeItem(key: LocalStorageKeys): void
removeItem(key: keyof LocalStorageValues
key: type LocalStorageKeys = keyof LocalStorageValues
LocalStorageKeys): void {
var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.removeItem(key: string): void
Removes the key/value pair with the given key, if a key/value pair with the given key exists.
Dispatches a storage event on Window objects holding an equivalent Storage object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/removeItem)removeItem(key: keyof LocalStorageValues
key);
}
static LocalStorageHandler.clear(): void
clear(): void {
var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)
A browser-compatible implementation of [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
Data is stored unencrypted in the file specified by the `--localstorage-file` CLI flag.
Any modification of this data outside of the Web Storage API is not supported.
Enable this API with the `--experimental-webstorage` CLI flag.localStorage.Storage.clear(): void
Removes all key/value pairs, if there are any.
Dispatches a storage event on Window objects holding an equivalent Storage object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/clear)clear();
}
}
- Composables: Extract logic into Vue composables for better reusability and maintainability
// composables/useDarkMode.ts
import { function ref<T>(value: T): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref, function watch<T, Immediate extends Readonly<boolean> = false>(source: WatchSource<T>, cb: WatchCallback<T, MaybeUndefined<T, Immediate>>, options?: WatchOptions<Immediate>): WatchHandle (+3 overloads)
watch } from 'vue';
import { import LocalStorageHandler
LocalStorageHandler } from './LocalStorageHandler';
export function function useDarkMode(): {
isDarkMode: any;
}
useDarkMode() {
const const isDarkMode: any
isDarkMode = ref<any>(value: any): any (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which
has a single property `.value` that points to the inner value.ref(import LocalStorageHandler
LocalStorageHandler.getItem('darkMode') ?? false);
watch<any, false>(sources: any, cb: WatchCallback<any, any>, options?: WatchOptions<false> | undefined): WatchHandle (+3 overloads)
watch(const isDarkMode: any
isDarkMode, (newValue: any
newValue) => {
import LocalStorageHandler
LocalStorageHandler.setItem('darkMode', newValue: any
newValue);
});
return { isDarkMode: any
isDarkMode };
}
You can check the full refactored example out here
Play with Vue on Vue Playground
Conclusion
This post explained the effective use of LocalStorage in Vue to manage user settings such as dark mode. We covered its basic operations, addressed common issues, and provided solutions to ensure robust and efficient application development. With these strategies, developers can create more responsive applications that effectively meet user needs.