DEV Community

Cover image for How to Persist User Data with LocalStorage in Vue
Alexander Opalic
Alexander Opalic

Posted on

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()

Diagram that explans LocalStorage

Using LocalStorage for Dark Mode Settings

In Vue, you can use LocalStorage to save a user's preference for dark mode in a component.

Picture that shows a button where user can toggle dark mode

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


Enter fullscreen mode Exit fullscreen mode

Addressing Issues with Initial Implementation

In simple scenarios, the approach works well, but it faces several challenges in larger applications:

  1. Type Safety and Key Validation: Always check and handle data from LocalStorage to prevent errors.
  2. Decoupling from LocalStorage: Avoid direct LocalStorage interactions in your components. Instead, use a utility service or state management for better code maintenance and testing.
  3. Error Handling: Manage exceptions like browser restrictions or storage limits properly as LocalStorage operations can fail.
  4. Synchronization Across Components: Use event-driven communication or shared state to keep all components updated with changes.
  5. 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
Enter fullscreen mode Exit fullscreen mode
  • 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();
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 };
}
Enter fullscreen mode Exit fullscreen mode

Diagram that shows how component and localStorage work together

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.

Top comments (1)

Collapse
 
richardevcom profile image
richardev

Nice article. I use Pinia on Vue 3 tho'.