npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@laststance/redux-storage-middleware

v0.2.1

Published

SSR-safe Redux middleware for LocalStorage persistence with hydration control

Readme

@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