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 class="dark-mode-toggle" @click="toggleDarkMode">
{{ isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode' }}
<span class="icon" v-html="isDarkMode ? moonIcon : sunIcon" />
</button>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
const isDarkMode = ref(JSON.parse(localStorage.getItem('darkMode') ?? 'false'))
const styleProperties = computed(() => ({
'--background-color': isDarkMode.value ? '#333' : '#FFF',
'--text-color': isDarkMode.value ? '#FFF' : '#333'
}))
const sunIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-sun" viewBox="0 0 16 16">
<path d="M8 4.41a3.59 3.59 0 1 1 0 7.18 3.59 3.59 0 0 1 0-7.18zM8 1a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0V1.5A.5.5 0 0 1 8 1zm0 12a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0v-1.5a.5.5 0 0 1 .5-.5zm6-6a.5.5 0 0 1 .5.5h1.5a.5.5 0 0 1 0 1H14.5a.5.5 0 0 1-.5-.5zm-12 0A.5.5 0 0 1 2 8H.5a.5.5 0 0 1 0-1H2a.5.5 0 0 1 .5.5zm9.396 5.106a.5.5 0 0 1 .708 0l1.06 1.06a.5.5 0 1 1-.708.708l-1.06-1.06a.5.5 0 0 1 0-.708zM3.146 3.854a.5.5 0 0 1 .708 0L4.914 5.56a.5.5 0 1 1-.708.708L3.146 4.562a.5.5 0 0 1 0-.708zm9.708 9.292a.5.5 0 0 1 .708 0L14.06 14.44a.5.5 0 0 1-.708.708l-1.06-1.06a.5.5 0 0 1 0-.708zM3.146 14.44a.5.5 0 0 1 0 .708l-1.06 1.06a.5.5 0 1 1-.708-.708l1.06-1.06a.5.5 0 0 1 .708 0z"/>
</svg>`
const moonIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-moon" viewBox="0 0 16 16">
<path d="M14.53 11.29c.801-1.422.852-3.108.172-4.614-.679-1.506-1.946-2.578-3.465-2.932a.5.5 0 0 0-.568.271A5.023 5.023 0 0 0 9 9.75c0 1.01.374 1.93.973 2.628a.5.5 0 0 0 .567.274 5.538 5.538 0 0 0 4.257-2.064.5.5 0 0 0-.267-.79z"/>
</svg>`
function applyStyles () {
for (const [key, value] of Object.entries(styleProperties.value)) {
document.documentElement.style.setProperty(key, value)
}
}
function toggleDarkMode () {
isDarkMode.value = !isDarkMode.value
localStorage.setItem('darkMode', JSON.stringify(isDarkMode.value))
applyStyles()
}
// On component mount, apply the stored or default styles
onMounted(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. ```ts // types/localStorageTypes.ts
export type UserSettings = {name: string}
export type LocalStorageValues = {
darkMode: boolean,
userSettings: UserSettings,
lastLogin: Date,
}
export type LocalStorageKeys = keyof LocalStorageValues
- **Utility Classes**: Create a utility class to manage all LocalStorage operations.
```ts
// utils/LocalStorageHandler.ts
import { LocalStorageKeys, LocalStorageValues } from '@/types/localStorageTypes';
export class LocalStorageHandler {
static getItem<K extends LocalStorageKeys>(key: K): LocalStorageValues[K] | null {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) as LocalStorageValues[K] : null;
} catch (error) {
console.error(`Error retrieving item from localStorage: ${error}`);
return null;
}
}
static setItem<K extends LocalStorageKeys>(key: K, value: LocalStorageValues[K]): void {
try {
const item = JSON.stringify(value);
localStorage.setItem(key, item);
} catch (error) {
console.error(`Error setting item in localStorage: ${error}`);
}
}
static removeItem(key: LocalStorageKeys): void {
localStorage.removeItem(key);
}
static clear(): void {
localStorage.clear();
}
}
- Composables: Extract logic into Vue composables for better reusability and maintainability ```ts // composables/useDarkMode.ts import { ref, watch } from 'vue'; import { LocalStorageHandler } from './LocalStorageHandler';
export function useDarkMode() {
const isDarkMode = ref(LocalStorageHandler.getItem('darkMode') ?? false);
watch(isDarkMode, (newValue) => {
LocalStorageHandler.setItem('darkMode', newValue);
});
return { isDarkMode };
}
![Diagram that shows how component and localStorage work together](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dxdlf9kz6erd380ednuo.png)
You can check the full refactored example out here
[Play with Vue on Vue Playground](https://play.vuejs.org/#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)
## 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.
Top comments (1)
Nice article. I use Pinia on Vue 3 tho'.