Nano Stores Persistent

repository·main·Indexed 18 days ago

https://github.com/nanostores/persistent

A lightweight plugin for the Nano Stores state manager that enables state persistence in localStorage or other storage engines. It provides automatic synchronization between browser tabs and includes specialized stores such as persistentAtom, persistentBoolean, persistentJSON, and persistentMap for granular key-value storage. The library supports custom storage and event engines via setPersistentEngine, built-in SSR support, and a dedicated test storage engine for in-memory testing.

Tokens
3.5K
Snippets
16
Records
16
Agent score
14%

What's inside @nanostores/persistent

  1. Configure synchronization between browser tabs

    main

    By default, changes to persistent stores are synchronized across browser tabs. To disable this behavior for a specific store, set the listen option to false in the configuration object.

    import { persistentAtom } from '@nanostores/persistent'
    
    // Synchronization is disabled for this store
    export const $draft = persistentAtom('draft', '', { listen: false })
  2. Test persistent stores with a fake storage engine

    main

    For testing environments, use the provided test API to replace localStorage with a fake engine. This allows you to manipulate keys and inspect storage state without a real browser environment.

    Available helpers:

    • useTestStorageEngine(): Activates the fake engine.
    • setTestStorageKey(key, value): Manually sets a value in the fake storage.
    • cleanTestStorage(): Clears the fake storage.
    • getTestStorage(): Returns the entire fake storage object.
    import {
      useTestStorageEngine,
      setTestStorageKey,
      cleanTestStorage,
      getTestStorage
    } from '@nanostores/persistent'
    
    import { $settings } from './storage.js'
    
    beforeAll(() => {
      useTestStorageEngine()
    })
    
    afterEach(() => {
      cleanTestStorage()
    })
    
    it('listens for changes', () => {
      setTestStorageKey('settings:locale', 'ru')
      expect($settings.get()).toEqual({ locale: 'ru' })
    })
  3. Install Nano Stores Persistent

    main

    Install the @nanostores/persistent package along with the core nanostores library using npm.

    npm install nanostores @nanostores/persistent
  4. Handle Server-Side Rendering (SSR)

    main

    Nano Stores Persistent has built-in SSR support. On the server, stores use empty objects instead of localStorage. To ensure the client has the correct state, you can manually initialize stores with data during the server lifecycle.

    if (isServer) {
      $locale.set(user.locale)
    }
  5. Use persistentBoolean for boolean values

    main

    A specialized wrapper for storing boolean values in localStorage.

    import { persistentBoolean } from '@nanostores/persistent'
    
    export const $reduceMotion = persistentBoolean('reduce-motion')
  6. Use persistentAtom for primitive values

    main

    Use persistentAtom to create a store that keeps a single primitive or object value under a single localStorage key. You can provide custom encode and decode functions to handle serialization (e.g., using JSON.stringify and JSON.parse).

    If the key is missing in localStorage, the store will use the provided initial value.

    import { persistentAtom } from '@nanostores/persistent'
    
    export const $shoppingCart = persistentAtom<Product[]>('cart', [], {
      encode: JSON.stringify,
      decode: JSON.parse
    })
    
    // Updating the value
    $shoppingCart.set([...$shoppingCart.get(), newProduct])
  7. Implement a custom Persistent Engine

    main

    You can replace localStorage with any other storage mechanism using setPersistentEngine.

    Your engine must provide:

    1. A storage object implementing set, get, and deleteProperty.
    2. An events object implementing addEventListener and removeEventListener.

    The events.perKey flag determines if PersistentMap should add individual listeners for each key or use a single global listener.

    import { setPersistentEngine, PersistentListener, PersistentEvent } from '@nanostores/persistent'
    
    // 1. Implement storage
    const storage = new Proxy({}, {
      set(target, name, value) { target[name] = value; return true },
      get(target, name) { return target[name] },
      deleteProperty(target, name) { delete target[name]; return true }
    })
    
    // 2. Implement events
    const events = {
      addEventListener(key: string, callback: PersistentListener) { /* ... */ },
      removeEventListener(key: string, callback: PersistentListener) { /* ... */ },
      perKey: false
    }
    
    setPersistentEngine(storage, events)
  8. Use persistentJSON for automatic JSON serialization

    main

    persistentJSON is a shortcut for persistentAtom that automatically handles JSON serialization and deserialization for objects, arrays, and primitives.

    If you omit the initial value argument, the store type will include null to account for the empty state.

    import { persistentJSON } from '@nanostores/persistent'
    
    const $cart = persistentJSON<Product[]>('cart', [])
    const $theme = persistentJSON<'dark' | 'light' | 'auto'>('theme', 'auto')
    
    // If initial value is omitted, type becomes Product[] | null
    const $comments = persistentJSON<Comment[]>('comments')
    
    $cart.set([...$cart.get(), newProduct])
    $theme.set('dark')
  9. Configure value encoding and decoding

    main

    You can use the encode and decode options in persistentAtom to transform values before they are written to or read from storage. This is useful for custom serialization logic.

    import { persistentAtom } from '@nanostores/persistent'
    
    export const $draft = persistentAtom('draft', [], {
      encode(value) {
        return JSON.stringify(value)
      },
      decode(value) {
        try {
          return JSON.parse(value)
        } catch() {
          return value
        }
      }
    })
  10. Use persistentMap for key-value storage

    main

    persistentMap stores each key of an object in a separate localStorage key. This is more efficient for objects where you want to update individual properties without re-writing the entire object.

    To update a specific key within the map, use the setKey method.

    import { persistentMap } from '@nanostores/persistent'
    
    export type SettingsValue = {
      sidebar: 'show' | 'hide'
      theme: 'dark' | 'light' | 'auto'
    }
    
    // Keys will be stored as 'settings:sidebar' and 'settings:theme'
    export const $settings = persistentMap<SettingsValue>('settings:', {
      sidebar: 'show',
      theme: 'auto'
    })
    
    // Update a specific key
    $settings.setKey('sidebar', 'hide')
  11. Test persistent stores with useTestStorageEngine()

    main

    For testing purposes, you can use useTestStorageEngine() to redirect all persistent stores to an in-memory test storage instead of localStorage. This prevents side effects in your test environment.

    Related Test APIs:

    • setTestStorageKey(key, newValue): Manually update the test storage and trigger events.
    • getTestStorage(): Retrieve the current state of the test storage.
    • cleanTestStorage(): Clear all keys from the test storage.
    import { useTestStorageEngine, setTestStorageKey } from '@nanostores/persistent'
    
    // Setup test environment
    useTestStorageEngine()
    
    // Simulate an external change
    setTestStorageKey('my-key', 'new-value')
  12. Create a persistent map with persistentMap()

    main

    Use persistentMap(prefix, initial, opts) to create a persistent Nano Store map. Instead of storing the entire map as one blob, it uses a prefix to store individual keys in the storage engine (e.g., if prefix is user:, keys in the map are stored as user:name, user:age, etc.).

    Features:

    • Granular Updates: Updating a single key via .setKey(key, value) updates only that specific entry in storage.
    • Bulk Updates: Calling .set(newObject) updates all keys in the object and removes keys from the store that are not present in the new object.
    • Syncing: It listens for changes to keys matching the prefix.

    Options:

    • encode: Function to transform values before saving. Defaults to identity.
    • decode: Function to transform values retrieved from storage. Defaults to identity.
    • listen: Boolean. If false, the store will not listen for external storage events.
    import { persistentMap } from '@nanostores/persistent'
    
    const settings = persistentMap('settings:', { theme: 'light' })
    
    // Update a single key
    settings.setKey('theme', 'dark')
    
    // Update multiple keys at once
    settings.set({ theme: 'dark', lang: 'en' })