Hone logo
Hone
Problems

Pinia Store Persistence

This challenge focuses on implementing persistence for your Pinia stores in a Vue 3 application using TypeScript. Maintaining the state of your application across page reloads is crucial for a seamless user experience, and this challenge will guide you through achieving that with Pinia.

Problem Description

Your task is to create a Pinia store that can automatically persist its state to localStorage and restore it when the application loads. This means that any changes made to the store's state should be saved, and upon a page refresh or a new session, the store should be initialized with the previously saved state.

Key Requirements:

  1. State Persistence: The entire state of a designated Pinia store must be saved to localStorage.
  2. State Restoration: When the Pinia store is initialized, it should check localStorage for existing state and use it if found.
  3. Real-time Updates: Changes to the store's state should trigger an update in localStorage asynchronously.
  4. TypeScript Integration: The solution must be implemented using TypeScript, ensuring type safety for the store and its persisted state.
  5. Modularization: The persistence logic should be encapsulated in a reusable way, ideally as a Pinia plugin.

Expected Behavior:

  • When a user interacts with the application and modifies the store's state, that state should be saved to localStorage.
  • If the user navigates away and returns, or refreshes the page, the store should load with the state it had before the reload.
  • If localStorage is empty or corrupted, the store should initialize with its default state.

Edge Cases:

  • Empty localStorage: The store should initialize with its default state.
  • Corrupted localStorage data: Handle potential errors during JSON parsing or if the stored data structure doesn't match the expected store state. The store should fall back to its default state.
  • Browser Incognito/Private Mode: localStorage behavior can differ. The persistence should gracefully handle situations where localStorage might not be available or persistent across sessions.

Examples

Example 1: Basic Counter Store

Let's assume you have a simple counter store:

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});

Input:

  1. User opens the application for the first time.
  2. User clicks the "Increment" button 3 times.

Expected Output (in localStorage after actions):

{
  "pinia_state": {
    "counter": {
      "count": 3
    }
  }
}

Explanation:

The counter store's state ({ count: 3 }) is saved under a key (e.g., "pinia_state") in localStorage.

Example 2: Counter Store Reload

Input:

  1. User performs actions from Example 1, saving the state {"pinia_state": {"counter": {"count": 3}}} to localStorage.
  2. User refreshes the browser.

Expected Output (after reload):

The counter store is initialized with count as 3.

Explanation:

Upon initialization, the persistence plugin detects the stored state in localStorage and uses it to hydrate the counter store.

Example 3: Multiple Stores with Persistence

Consider a user profile store alongside the counter store:

// stores/user.ts
import { defineStore } from 'pinia';

interface UserState {
  name: string;
  isLoggedIn: boolean;
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    name: '',
    isLoggedIn: false,
  }),
  actions: {
    setUser(name: string) {
      this.name = name;
      this.isLoggedIn = true;
    },
    logout() {
      this.name = '';
      this.isLoggedIn = false;
    },
  },
});

Input:

  1. User sets their name to "Alice" and logs in.
  2. User increments the counter 5 times.
  3. User refreshes the browser.

Expected Output (in localStorage after actions):

{
  "pinia_state": {
    "counter": {
      "count": 5
    },
    "user": {
      "name": "Alice",
      "isLoggedIn": true
    }
  }
}

Explanation:

The persistence plugin should handle multiple stores, saving and restoring their states collectively under a common key.

Constraints

  • The solution should primarily target localStorage. While other storage mechanisms can be considered for extension, the core challenge is localStorage.
  • The solution should be implemented as a Pinia plugin for maximum reusability and adherence to Pinia's plugin architecture.
  • The persistence should occur asynchronously to avoid blocking the main thread during state updates.
  • The storage key for the persisted state should be configurable, with a reasonable default.
  • The plugin should allow for selective persistence of stores or specific state properties if needed (though the core challenge is full store persistence).

Notes

  • Consider using a library like lodash for deep cloning states if you encounter issues with nested objects and reactivity.
  • Think about how you'll handle different data types stored in the state. JSON.stringify and JSON.parse are your friends here, but be mindful of their limitations (e.g., Date objects being converted to strings).
  • The goal is to make this persistence solution as seamless and automatic as possible.
  • You might want to consider debouncing or throttling the localStorage writes to further optimize performance.
  • For a production-ready solution, you might also want to consider adding features like expiration times for stored data or encryption.
Loading editor...
typescript