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

@chromahq/store

v1.0.57

Published

Centralized, persistent store for Chrome extensions using zustand, accessible from service workers and React, with chrome.storage.local persistence.

Readme

@chromahq/store

🏪 Simple, powerful state management for Chrome extensions with automatic synchronization between service worker and UI contexts.

✨ Features

  • 🔄 Auto-Sync: Service worker ↔ Popup ↔ Content scripts
  • 💾 Auto-Persist: All stores automatically persist to Chrome storage
  • 🎯 Zero Config: Smart context detection - no complex setup
  • 🎨 Modern API: Clean, fluent builder pattern
  • 🔒 Type Safe: Full TypeScript support with excellent DX
  • ⚡ Fast: Optimistic updates with background sync

🚀 Quick Setup

Install

npm install @chromahq/store @chromahq/core

� Usage

1. Define Your Slices

// src/slices/counter.ts
import type { StateCreator } from 'zustand';

export interface CounterSlice {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

export const counterSlice: StateCreator<CounterSlice> = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
});

// Export combined type for TypeScript
export type RootState = CounterSlice; // Add more slices with &

2. Service Worker Setup

// src/service-worker.ts
import '@abraham/reflection';
import { StoreDefinition } from '@chromahq/store';
import { bootstrap } from '@chromahq/core';
import { counterSlice } from './slices/counter';

const store: StoreDefinition = {
  name: 'app',
  slices: [counterSlice],
  // Persistence is automatic - no config needed!
};

bootstrap().withStore(store).create();

3. React UI Setup

// src/hooks/useAppStore.ts
import { RootState } from '../slices/counter';
import { useBridge } from '@chromahq/react';
import { CentralStore, createStore } from '@chromahq/store';
import { useEffect, useState } from 'react';

export function useAppStore() {
  const { bridge } = useBridge();
  const [store, setStore] = useState<CentralStore<RootState>>();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function initStore() {
      if (!bridge) return;

      const store = await createStore<RootState>('app')
        .withSlices(counterSlice)
        .withBridge(bridge)
        .create();

      setStore(store);
      setLoading(false);
    }

    initStore();
  }, [bridge]);

  return { store, loading };
}
// src/components/Counter.tsx
import { useCentralStore } from '@chromahq/store';
import { useAppStore } from '../hooks/useAppStore';

export function Counter() {
  const { store, loading } = useAppStore();

  // Use the store with a selector
  const count = useCentralStore(store!, (state) => state.count);
  const increment = useCentralStore(store!, (state) => state.increment);
  const decrement = useCentralStore(store!, (state) => state.decrement);

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

🏗 Architecture

Service Worker (Source of Truth)

  • ServiceWorkerStore: Real Zustand store with automatic Chrome storage persistence
  • Message Handlers: Auto-registered for cross-context communication
  • State Authority: The single source of truth for all application state

UI Contexts (React/Popup/Content Scripts)

  • BridgeStore: Lightweight proxy that connects to service worker via bridge
  • Reactive Updates: Automatically syncs with service worker state changes
  • Optimistic Updates: Immediate UI feedback with background synchronization

🎯 Key Benefits

Simple & Clean

  • No plugin system complexity - just slices and bridges
  • Automatic persistence - every store persists without configuration
  • Smart context detection - creates the right store type automatically

Developer Experience

  • TypeScript first - excellent type inference and safety
  • Familiar API - built on Zustand, same patterns you know
  • Zero boilerplate - minimal setup, maximum functionality

Performance

  • Optimistic updates - UI responds immediately
  • Background sync - service worker handles persistence
  • Efficient bridge - only sends serializable data, executes functions locally

📚 API Reference

Core Functions

// Create a store builder
createStore<T>(name?: string): StoreBuilder<T>

// StoreBuilder methods
.withSlices(...slices): StoreBuilder<T>    // Add state slices
.withBridge(bridge): StoreBuilder<T>       // Connect to service worker (UI only)
.create(): Promise<CentralStore<T>>        // Create the store

// React hooks
useCentralStore<T, U>(store, selector): U  // Subscribe to state
useCentralDispatch<T>(store): SetState<T>  // Get state updater

Types

// Store definition for service worker bootstrap
interface StoreDefinition {
  name: string;
  slices: StateCreator<any, [], [], any>[];
}

// Central store interface (both ServiceWorkerStore and BridgeStore)
interface CentralStore<T> {
  getState(): T;
  setState(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: boolean): void;
  subscribe(listener: (state: T, prevState: T) => void): () => void;
  ready: Promise<void>;
}

🔧 Advanced Usage

Multiple Stores

// Service worker
const userStore: StoreDefinition = { name: 'user', slices: [userSlice] };
const settingsStore: StoreDefinition = { name: 'settings', slices: [settingsSlice] };

bootstrap().withStore(userStore).withStore(settingsStore).create();

// React
const userStore = await createStore<UserState>('user')
  .withSlices(userSlice)
  .withBridge(bridge)
  .create();
const settingsStore = await createStore<SettingsState>('settings')
  .withSlices(settingsSlice)
  .withBridge(bridge)
  .create();

Context Providers

// Create typed hooks with context
import { createStoreHooks } from '@chromahq/store';

const { StoreProvider, useStore } = createStoreHooks<RootState>();

function App() {
  const { store } = useAppStore();

  return (
    <StoreProvider store={store}>
      <MyComponent />
    </StoreProvider>
  );
}

function MyComponent() {
  // No need to pass store around - uses context
  const count = useStore(state => state.count);
  const increment = useStore(state => state.increment);

  return <button onClick={increment}>{count}</button>;
}

🎉 That's it! You now have powerful, type-safe state management across your entire Chrome extension with automatic persistence and synchronization. return ( ); }


### 3. Use in Components

```typescript
// src/popup/components/Counter.tsx
import React from 'react';
import { useStore } from '../../hooks/useAppStore';

export function Counter() {
  // Select specific state (automatically typed!)
  const count = useStore(state => state.count);
  const { increment, decrement, reset } = useStore(state => ({
    increment: state.increment,
    decrement: state.decrement,
    reset: state.reset
  }));

  return (
    <div className="counter">
      <h2>Counter: {count}</h2>
      <div className="buttons">
        <button onClick={increment}>+1</button>
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}
// src/popup/components/UserProfile.tsx
import React from 'react';
import { useStore } from '../../hooks/useAppStore';

export function UserProfile() {
  const { user, isAuthenticated } = useStore(state => ({
    user: state.user,
    isAuthenticated: state.isAuthenticated
  }));
  const { login, logout } = useStore(state => ({
    login: state.login,
    logout: state.logout
  }));

  const handleLogin = () => {
    login({
      name: 'John Doe',
      email: '[email protected]',
      id: '12345'
    });
  };

  if (!isAuthenticated) {
    return (
      <div className="login">
        <h3>Please log in</h3>
        <button onClick={handleLogin}>Login</button>
      </div>
    );
  }

  return (
    <div className="profile">
      <h3>Welcome, {user.name}!</h3>
      <p>Email: {user.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

🔄 How Sync Works

Automatic Synchronization

// Any change in service worker...
store.getState().increment(); // count: 0 → 1

// ...automatically appears in React components!
// No manual sync code needed ✨

The Magic Behind The Scenes

  1. Service Worker creates real store with persistence
  2. React Components create bridge store with same name
  3. @chromahq/core bridge automatically syncs all changes
  4. State updates flow instantly between contexts
  5. Persistence ensures state survives browser restarts

🛠️ Advanced Usage

Custom Plugins

// src/app/stores/plugins.ts
import type { StorePlugin } from '@chromahq/store';

// Analytics plugin
export const analyticsPlugin: StorePlugin = {
  name: 'analytics',
  priority: 50,
  async setup(store, config) {
    store.subscribe((state, prevState) => {
      // Track state changes
      chrome.runtime.sendMessage({
        type: 'ANALYTICS_EVENT',
        data: { store: config.name, state },
      });
    });
  },
};

// Logging plugin
export const loggingPlugin: StorePlugin = {
  name: 'logging',
  priority: 10, // Higher priority = runs first
  async setup(store, config) {
    console.log(`🏪 Store "${config.name}" initialized`);

    store.subscribe((state, prevState) => {
      console.log('State change:', { from: prevState, to: state });
    });
  },
};

Enhanced Store Definition

// src/app/stores/app.store.ts
import { counterSlice, userSlice } from '../slices';
import { analyticsPlugin, loggingPlugin } from './plugins';
import type { StoreDefinition } from '@chromahq/store';

const store: StoreDefinition = {
  name: 'app',
  slices: [counterSlice, userSlice],
  persistence: {
    name: 'my-extension-state',
    version: 2,
    migrate: (state, version) => {
      // Handle state migrations
      if (version < 2) {
        return { ...state, newField: 'default' };
      }
      return state;
    },
  },
  plugins: [loggingPlugin, analyticsPlugin],
  config: {
    apiUrl: 'https://api.myservice.com',
    debugMode: true,
  },
};

export default store;

Multiple Stores

// src/app/stores/user.store.ts
import { userSlice, authSlice } from '../slices';

export default {
  name: 'user',
  slices: [userSlice, authSlice],
  persistence: { name: 'user-data' },
};

// src/app/stores/settings.store.ts
import { settingsSlice, themeSlice } from '../slices';

export default {
  name: 'settings',
  slices: [settingsSlice, themeSlice],
  persistence: { name: 'user-settings' },
};

// src/app/stores/cache.store.ts
import { cacheSlice } from '../slices';
import { ttlPlugin } from './plugins';

export default {
  name: 'cache',
  slices: [cacheSlice],
  plugins: [ttlPlugin],
};

🎯 Best Practices

1. Store Organization

// ✅ Good: Feature-based slices
const userSlice = (set, get) => ({
  /* user logic */
});
const settingsSlice = (set, get) => ({
  /* settings logic */
});

// ❌ Avoid: One giant slice
const everythingSlice = (set, get) => ({
  /* 500 lines of code */
});

2. State Selection

// ✅ Good: Specific selectors
const count = useStore((state) => state.count);
const userName = useStore((state) => state.user?.name);

// ❌ Avoid: Selecting entire state
const everything = useStore((state) => state); // Causes unnecessary re-renders

3. Store Names

// ✅ Good: Descriptive and consistent
createStore('user-preferences'); // Service worker
createStore('user-preferences'); // React (same name!)

// ❌ Avoid: Generic or mismatched names
createStore('store'); // Service worker
createStore('data'); // React (different name!)

4. Error Handling

// ✅ Good: Handle connection states
function AppContent() {
  const store = useAppStore();

  if (!store) {
    return <LoadingSpinner />;
  }

  return <MainApp store={store} />;
}

// ❌ Avoid: Assuming store is always ready
function App() {
  const store = useAppStore();
  return <StoreProvider store={store}>...</StoreProvider>; // Might be null!
}

🚨 Troubleshooting

Store Not Syncing

  • ✅ Ensure both stores use the exact same name
  • ✅ Check that service worker store is created first
  • ✅ Verify @chromahq/core bridge is initialized with create()

React Components Not Updating

  • ✅ Use specific selectors: state => state.count not state => state
  • ✅ Ensure components are wrapped in <StoreProvider>
  • ✅ Check that bridge connection is established

State Not Persisting

  • ✅ Add .withPersistence({ name: 'unique-name' }) to service worker store
  • ✅ Ensure service worker has storage permissions in manifest
  • ✅ Check Chrome DevTools → Application → Storage → Local Storage

TypeScript Errors

  • ✅ Define interfaces for your slices
  • ✅ Use createStoreHooks<YourStateType>() for typed hooks
  • ✅ Ensure same state shape in service worker and React

📦 Package Info

  • Main API: createStore() with plugin system
  • React Integration: createStoreHooks() for type-safe hooks
  • Auto-Sync: Works with @chromahq/core bridge
  • Persistence: Built-in Chrome storage support
  • TypeScript: Full type safety throughout

📄 License

MIT