How to Persist User Data with LocalStorage in Vue

How to Persist User Data with LocalStorage in Vue

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.

// 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.

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

// 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 };
}

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.

Leave a Reply

Your email address will not be published. Required fields are marked *