Skip to content

laststance/redux-strorage-middeware

Repository files navigation

@laststance/redux-storage-middleware

npm version License: MIT TypeScript

SSR-safe Redux Toolkit middleware for localStorage persistence with selective slice hydration and performance optimization.

Highlights

  • 🔒 SSR-Safe: Works seamlessly with Next.js App Router and Server Components
  • 🎯 Selective Persistence: Choose which slices to persist with slices option
  • Performance Optimized: Debounced/throttled writes, idle callback support
  • 📦 Simple API: Minimal configuration, maximum productivity
  • 🧪 Battle-Tested: 100+ tests, high coverage, E2E verified with 5000+ items

Table of Contents


Installation

pnpm add @laststance/redux-storage-middleware

# Optional serializers
pnpm add superjson     # For Date/Map/Set support
pnpm add lz-string     # For compression

Peer Dependencies:

{
  "@reduxjs/toolkit": "^2.0.0",
  "react-redux": "^9.0.0"
}

Quick Start

import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { createStorageMiddleware } from '@laststance/redux-storage-middleware'

interface AppState {
  emails: EmailsState
  settings: SettingsState
}

// Create root reducer
const rootReducer = combineReducers({
  emails: emailReducer,
  settings: settingsReducer,
})

// Create middleware, reducer, and API
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer, // Required: pass your root reducer
  key: 'my-app-state',
  slices: ['emails', 'settings'],
})

// Configure store with returned reducer (already hydration-wrapped)
export const store = configureStore({
  reducer, // Use the returned reducer
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(middleware),
})

// Hydration happens automatically on client
// Export API for manual control
export { api as storageApi }

API Reference

createStorageMiddleware<S>(options)

Creates the storage middleware and returns both the middleware and a control API.

Configuration Options

Option Type Default Description
rootReducer Reducer<S, AnyAction> required Root reducer to wrap with hydration
key string required localStorage key
slices (keyof S)[] undefined State slices to persist (all if undefined)

Performance Options

Option Type Default Description
performance.debounceMs number 300 Debounce delay for saves
performance.throttleMs number undefined Throttle interval (overrides debounce)
performance.useIdleCallback boolean false Use requestIdleCallback
performance.idleTimeout number 1000 Fallback timeout for idle callback

Lifecycle Callbacks

Callback Signature Description
onHydrationComplete (state: S) => void Called when hydration completes
onSaveComplete (state: S) => void Called after each save
onError (error, operation) => void Error handler for load/save/clear

Returns

interface StorageMiddlewareResult<S> {
  middleware: Middleware<object, S> // Redux middleware
  reducer: Reducer<S, AnyAction> // Hydration-wrapped reducer (use this in configureStore)
  api: HydrationApi<S> // Control API
}

Hydration API

The api object returned from createStorageMiddleware provides control methods:

interface HydrationApi<T> {
  // State management
  rehydrate(): Promise<void> // Manual hydration trigger
  hasHydrated(): boolean // Check if hydration completed
  getHydrationState(): HydrationState // 'idle' | 'hydrating' | 'hydrated' | 'error'
  getHydratedState(): T | null // Access the hydrated state

  // Storage control
  clearStorage(): void // Remove persisted state

  // Callbacks (returns unsubscribe function)
  onHydrate(cb: (state: T) => void): () => void
  onFinishHydration(cb: (state: T) => void): () => void
}

Hydration States

State Description
idle Initial state, hydration not yet attempted
hydrating Hydration in progress
hydrated Hydration completed successfully
error Hydration failed (storage/parse error)

Storage Backends

Factory functions for different storage backends:

import {
  createSafeLocalStorage, // SSR-safe localStorage wrapper
  createSafeSessionStorage, // SSR-safe sessionStorage wrapper
  createNoopStorage, // No-op storage (SSR fallback)
  createMemoryStorage, // In-memory storage for testing
  toAsyncStorage, // Convert sync to async wrapper
} from '@laststance/redux-storage-middleware'

// Utility functions
import {
  isValidStorage, // Type guard for StateStorage
  getStorageSize, // Get item size in bytes
  getRemainingStorageQuota, // Estimate quota remaining
} from '@laststance/redux-storage-middleware'

Serializers

JSON Serializer (Default)

import {
  createJsonSerializer, // Basic JSON serializer
  createEnhancedJsonSerializer, // With Date/Map/Set support
  defaultJsonSerializer, // Default instance
} from '@laststance/redux-storage-middleware'

const serializer = createJsonSerializer({
  replacer: (key, value) => value, // Custom replacer
  reviver: (key, value) => value, // Custom reviver
  space: 2, // Indent for debugging
})

SuperJSON Serializer

Handles Date, Map, Set, undefined, BigInt automatically.

import {
  initSuperJsonSerializer,
  createSuperJsonSerializer,
  isSuperJsonLoaded,
} from '@laststance/redux-storage-middleware'

// Initialize once at app startup
await initSuperJsonSerializer()

// Create serializer
const serializer = createSuperJsonSerializer<AppState>()

Compressed Serializer

LZ-String based compression for large state.

import {
  initCompressedSerializer,
  createCompressedSerializer,
  isLZStringLoaded,
  getCompressionRatio,
} from '@laststance/redux-storage-middleware'

await initCompressedSerializer()

const serializer = createCompressedSerializer<AppState>({
  format: 'utf16' | 'base64' | 'uri', // default: 'utf16'
})

Advanced Usage

SSR Integration

// Next.js App Router pattern
'use client'

import { useEffect, useState } from 'react'
import { Provider } from 'react-redux'
import { store, storageApi } from './store'

export function StoreProvider({ children }) {
  const [hydrated, setHydrated] = useState(false)

  useEffect(() => {
    // Subscribe to hydration completion
    const unsubscribe = storageApi.onFinishHydration(() => {
      setHydrated(true)
    })

    // Check if already hydrated
    if (storageApi.hasHydrated()) {
      setHydrated(true)
    }

    return unsubscribe
  }, [])

  if (!hydrated) {
    return <LoadingSpinner />
  }

  return <Provider store={store}>{children}</Provider>
}

Performance

Benchmark Results (1000 emails)

Metric Value
JSON.stringify 0.32ms
JSON.parse 0.41ms
Storage Size ~460KB
Full Round-trip 0.74ms
Debounce Overhead <0.001ms

10 Optimization Approaches

We benchmarked 10 different localStorage optimization strategies:

Approach Write (ms) Read (ms) Size (KB) Notes
1. Native JSON 0.39 0.38 921 Baseline - fast, no type preservation
2. Type Preservation 1.48 1.47 921 Preserves Date, Map, Set
3. Compression 1.12 1.28 1812 Base64 overhead increases size
4. Selective Slices 0.31 0.38 921 Only persist critical slices
5. Debounced Writes 3.06 0.40 461 10 changes → 1 write
6. Throttled Writes 1.56 0.42 461 Rate-limited writes
7. Differential Updates 0.64 0.39 0.1 Only store changed portions
8. Chunked Storage 0.34 0.42 921 Split into chunks
9. Minimal Serialization 1.17 0.45 845 Short keys (~8% smaller)
10. Lazy Hydration 0.33 0.001 0.1 Meta-first, full data on demand

Large Dataset Performance (5000 emails)

Metric Value
Storage Size ~4.5MB
Write Time ~2ms
Read Time ~2ms
Hydration (E2E) ~500ms

Recommendations

Use Case Recommended Approach
Most apps Debounced writes (default 300ms) - reduces writes by 10x
Large datasets Lazy hydration - near-instant initial load
Frequent updates Differential updates - minimal data transfer
Type-rich data Type preservation (SuperJSON) if you need Date/Map/Set

Running Benchmarks

# Core benchmarks
npx tsx benchmarks/benchmark.ts

# 10 optimization approaches
npx tsx benchmarks/optimization-approaches.ts

Testing

Unit Tests (Vitest)

pnpm test           # Watch mode
pnpm test:run       # Single run
pnpm test:coverage  # With coverage

Coverage: 145 tests with 80%+ coverage across 9 test files:

Category Tests Coverage
Core Middleware 29 Initialization, hydration, callbacks
Storage Layer 19 localStorage, memory, async wrappers
JSON Serializer 18 Basic, enhanced, replacers/revivers
SuperJSON Serializer 16 Async init, type handling, errors
Compressed Serializer 19 LZ-String, formats, compression
Utilities 24 Debounce, throttle, SSR detection
Package Exports 14 All public API validation

E2E Tests (Playwright)

pnpm build && pnpm test:e2e

24 tests including:

  • 1000+ email load testing (<5s requirement)
  • localStorage persistence verification
  • Page reload hydration (<3s requirement)
  • Debounce optimization verification

Examples

Gmail Clone Demo

A production-grade demo showing 5000+ email persistence:

Location: examples/gmail-clone

Stack:

  • Next.js 14 (App Router)
  • Redux Toolkit 2.11
  • shadcn/ui + Tailwind CSS
  • Playwright E2E tests

Features:

  • Real localStorage persistence
  • Hydration status indicator
  • Search & filter functionality
  • Performance metrics display

Configuration:

const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer, // Pass your root reducer
  key: 'gmail-clone-state',
  slices: ['emails'],
  performance: {
    debounceMs: 300,
    useIdleCallback: false, // For E2E predictability
  },
  onHydrationComplete: (state) => {
    console.log('Hydrated:', state.emails?.emails?.length)
  },
})

Run the demo:

cd examples/gmail-clone
pnpm dev

TypeScript Support

Full TypeScript support with generic state typing:

// State type inference
const { middleware, reducer, api } = createStorageMiddleware<RootState>({
  rootReducer, // Required: pass your root reducer
  key: 'app',
  slices: ['user', 'settings'], // Type-checked against RootState keys
})

// Hydration API is typed
const state: RootState | null = api.getHydratedState()

Action Types

import {
  ACTION_HYDRATE_START,
  ACTION_HYDRATE_COMPLETE,
  ACTION_HYDRATE_ERROR,
  type StorageMiddlewareAction,
} from '@laststance/redux-storage-middleware'

Note: The returned reducer from createStorageMiddleware() is already hydration-wrapped. No manual wrapper is needed.


Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new functionality
  4. Ensure all tests pass: pnpm test:run
  5. Run linting: pnpm lint
  6. Submit a pull request

Development

# Install dependencies
pnpm install

# Run tests in watch mode
pnpm test

# Type checking
pnpm typecheck

# Build
pnpm build

# Run Gmail Clone example
cd examples/gmail-clone && pnpm dev

License

MIT © Laststance.io


Related