TypeScript Zustand Chrome Extension React Vite

Zustand + Chrome Storage

Active GitHub →

A pattern for using Zustand with Chrome's Storage API as a single source of truth in browser extensions

Zustand Chrome Storage - State management for Chrome extensions

Zustand + Chrome Storage

A reference implementation for state management in Chrome Extensions using Zustand with Chrome’s Storage API as the persistence layer.

The Problem

Chrome Extensions have a unique constraint: code runs in multiple isolated contexts (popup, background service worker, content scripts) that can’t share memory. Traditional React state management assumes a single JavaScript context, which breaks down in extensions.

Common approaches have drawbacks:

  • Redux + webext-redux — heavyweight, complex setup, overkill for most extensions
  • Raw Chrome Storage — no reactivity, manual subscriptions, easy to create race conditions
  • Context API — doesn’t work across extension contexts

The Solution

Zustand’s vanilla (non-React) store can be configured to:

  1. Persist state to Chrome Storage
  2. Subscribe to storage changes from other contexts
  3. Provide reactive updates to React components

This gives you the developer experience of Zustand with automatic sync across all extension contexts.

How It Works

import { createStore } from "zustand/vanilla";
import { immer } from "zustand/middleware/immer";

interface AppState {
  favorites: string[];
  addFavorite: (id: string) => void;
}

// Create a vanilla store (works outside React)
const store = createStore<AppState>()(
  immer((set) => ({
    favorites: [],
    addFavorite: (id) =>
      set((state) => {
        state.favorites.push(id);
      }),
  })),
);

// Sync to Chrome Storage on state changes
store.subscribe((state) => {
  chrome.storage.local.set({ appState: state });
});

// Listen for changes from other contexts
chrome.storage.onChanged.addListener((changes) => {
  if (changes.appState) {
    store.setState(changes.appState.newValue);
  }
});

The popup, background script, and content scripts all create their own store instance, but Chrome Storage keeps them synchronized. Changes in one context propagate to all others.

Key Patterns

PatternPurpose
Vanilla storeWorks in non-React contexts (background scripts)
Immer middlewareErgonomic immutable updates
Storage listenerReact to changes from other contexts
Selective persistenceOnly persist what needs to survive restarts

Production Use

This pattern powers the Sovrn Commerce Chrome Extension, where it handles:

  • User authentication state across popup and content scripts
  • Cached API responses with TTL-based invalidation
  • User preferences that persist across browser sessions

Getting Started

gh repo clone drewalth/chrome-extension-zustand
cd chrome-extension-zustand
npm install
npm run dev

Load the extension in Chrome:

  1. Navigate to chrome://extensions/
  2. Enable “Developer mode”
  3. Click “Load unpacked” and select the dist directory

Tech Stack