@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.
Maintainers
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 updaterTypes
// 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
- Service Worker creates real store with persistence
- React Components create bridge store with same name
- @chromahq/core bridge automatically syncs all changes
- State updates flow instantly between contexts
- 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-renders3. 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/corebridge is initialized withcreate()
React Components Not Updating
- ✅ Use specific selectors:
state => state.countnotstate => 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/corebridge - Persistence: Built-in Chrome storage support
- TypeScript: Full type safety throughout
📄 License
MIT
