npm package: @laststance/redux-storage-middleware
Description: SSR-safe Redux middleware for LocalStorage persistence with hydration control
Author: Laststance.io
Latest version: 0.3.2

# @laststance/redux-storage-middleware

[![npm version](https://img.shields.io/npm/v/@laststance/redux-storage-middleware)](https://www.npmjs.com/package/@laststance/redux-storage-middleware)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)

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
- 🔄 **Version Migration**: Schema versioning with custom migration functions
- 🧪 **Battle-Tested**: 160+ tests, high coverage, E2E verified with 5000+ items

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
  - [createStorageMiddleware](#createstoragemiddlewaresoptions)
  - [Hydration API](#hydration-api)
  - [Storage Backends](#storage-backends)
  - [Serializers](#serializers)
- [Advanced Usage](#advanced-usage)
  - [SSR Integration](#ssr-integration)
  - [Version Migration](#version-migration)
  - [Custom Storage Backend](#custom-storage-backend)
  - [Custom Merge Strategy](#custom-merge-strategy)
- [Performance](#performance)
  - [Benchmark Results](#benchmark-results)
  - [10 Optimization Approaches](#10-optimization-approaches)
  - [Recommendations](#recommendations)
- [Testing](#testing)
- [Examples](#examples)
- [TypeScript Support](#typescript-support)
- [Contributing](#contributing)
- [License](#license)

---

## Installation

```bash
pnpm add @laststance/redux-storage-middleware

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

**Peer Dependencies:**

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

---

## Quick Start

```typescript
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)          |
| `storage`     | `SyncStorage`                                           | `createSafeLocalStorage()` | Custom storage backend                              |
| `serializer`  | `Serializer`                                            | `defaultJsonSerializer`    | Custom serializer for state persistence             |
| `version`     | `number`                                                | `0`                        | Schema version — increment when state shape changes |
| `migrate`     | `(state: Partial<S>, oldVersion: number) => Partial<S>` | `undefined`                | Migration function for version mismatches           |
| `merge`       | `(persisted: Partial<S>, current: S) => S`              | shallow merge              | Custom merge strategy for hydration                 |

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

```typescript
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:

```typescript
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:

```typescript
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)

```typescript
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.

```typescript
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.

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

```typescript
// 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>
}
```

### Version Migration

When your state shape changes between releases, use `version` and `migrate` to handle the transition:

```typescript
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer,
  key: 'my-app-state',
  slices: ['settings'],
  version: 2, // Increment when state shape changes
  migrate: (state, oldVersion) => {
    if (oldVersion < 1) {
      // v0 → v1: added theme field
      state.settings = { ...state.settings, theme: 'system' }
    }
    if (oldVersion < 2) {
      // v1 → v2: renamed 'lang' to 'locale'
      const { lang, ...rest } = state.settings as any
      state.settings = { ...rest, locale: lang ?? 'en' }
    }
    return state
  },
})
```

**Behavior:**

| Scenario                              | What happens                                      |
| ------------------------------------- | ------------------------------------------------- |
| Stored version matches config         | Normal hydration, no migration                    |
| Version mismatch + `migrate` provided | Runs migration, saves migrated state to storage   |
| Version mismatch + no `migrate`       | Clears storage (safe default), uses initial state |
| Migration throws an error             | Clears storage, calls `onError` callback          |
| Legacy data without version field     | Treated as version `0`                            |

### Custom Storage Backend

```typescript
import {
  createSafeSessionStorage,
  createMemoryStorage,
} from '@laststance/redux-storage-middleware'

// Use sessionStorage instead of localStorage
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer,
  key: 'my-app-state',
  storage: createSafeSessionStorage(),
})

// Use in-memory storage for testing
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer,
  key: 'test-state',
  storage: createMemoryStorage(),
})
```

### Custom Merge Strategy

```typescript
import { deepMerge } from '@laststance/redux-storage-middleware'

// Deep merge for nested state (default is shallow merge)
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer,
  key: 'my-app-state',
  merge: deepMerge,
})

// Custom merge: keep some fields from current state
const { middleware, reducer, api } = createStorageMiddleware<AppState>({
  rootReducer,
  key: 'my-app-state',
  merge: (persisted, current) => ({
    ...current,
    ...persisted,
    settings: { ...current.settings, ...persisted.settings },
  }),
})
```

---

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

```bash
# Core benchmarks
npx tsx benchmarks/benchmark.ts

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

---

## Testing

### Unit Tests (Vitest)

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

**Coverage:** 160 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)

```bash
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:**

```typescript
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:**

```bash
cd examples/gmail-clone
pnpm dev
```

---

## TypeScript Support

Full TypeScript support with generic state typing:

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

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

```bash
# 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](https://github.com/laststance)

---

## Related

- [Redux Toolkit](https://redux-toolkit.js.org/)
- [redux-persist](https://github.com/rt2zz/redux-persist) - Original inspiration
- [zustand/persist](https://github.com/pmndrs/zustand) - Middleware pattern reference
- [jotai/atomWithStorage](https://jotai.org/docs/utilities/storage) - SSR patterns