@hpkv/zustand-multiplayer v0.3.1
Zustand Multiplayer Middleware

Real-time state synchronization for Zustand stores. Build collaborative applications with automatic state sharing across multiple clients.
🚀 Quick Start • 📖 Examples • 🔧 API Reference
Installation
npm install @hpkv/zustand-multiplayer zustandPrerequisites
Quick Start
1. Create a Live Poll store
// store.js
import { create } from 'zustand';
import { multiplayer } from '@hpkv/zustand-multiplayer';
export const usePollStore = create(
multiplayer(
(set) => ({
votes: { pizza: 0, burger: 0, tacos: 0 },
vote: (option) => set((state) => ({
votes: {
...state.votes,
[option]: state.votes[option] + 1
}
})),
}),
{
namespace: 'live-poll',
apiBaseUrl: 'YOUR_HPKV_BASE_URL',
tokenGenerationUrl: '/api/generate-token',
}
)
);2. Create Token Endpoint
// Your backend API endpoint
import { TokenHelper } from '@hpkv/zustand-multiplayer';
const tokenHelper = new TokenHelper(
process.env.HPKV_API_KEY,
process.env.HPKV_API_BASE_URL
);
// In your POST /api/generate-token handler
async function handleTokenRequest(requestBody) {
try {
const response = await tokenHelper.processTokenRequest(requestBody);
return response; // { namespace: "live-poll", token: "eyJ..." }
} catch (error) {
return { error: error.message };
}
}📖 See Token API Guide for more details on the details and implementations in Express, Next.js, Fastify, and other frameworks.
3. Use in Your App
// App.js
import { usePollStore } from './store';
function App() {
const { votes, vote } = usePollStore();
return (
<div>
<h1>What's your favorite food? 🍕</h1>
<button onClick={() => vote('pizza')}>Pizza ({votes.pizza})</button>
<button onClick={() => vote('burger')}>Burger ({votes.burger})</button>
<button onClick={() => vote('tacos')}>Tacos ({votes.tacos})</button>
<p>👆 Vote and watch results update live across all devices!</p>
</div>
);
}🎉 That's it! Creating an online voting application is that easy!
Multiplayer State & Methods
Every store created with the multiplayer middleware provides a multiplayer object with state and methods for managing the connection and synchronization:
// Access multiplayer object from any store
const multiplayer = useMyStore((state) => state.multiplayer);
// Connection state (reactive)
console.log(multiplayer.connectionState);
// Manual control methods
multiplayer.hydrate(); // Refresh from server
multiplayer.clearStorage(); // Clear local data
multiplayer.connect(); // Reconnect
multiplayer.disconnect(); // DisconnectAvailable State:
connectionState- Reactive connection state object
Available Methods:
hydrate()- Manually sync with server stateclearStorage()- Clear all local stored dataconnect()- Establish connectiondisconnect()- Close connectiongetMetrics()- Get performance statistics
Non-React Usage
Vanilla JavaScript
import { createStore } from 'zustand/vanilla';
import { multiplayer } from '@hpkv/zustand-multiplayer';
// Create store without React hooks
const gameStore = createStore(
multiplayer(
(set, get) => ({
players: {},
gameState: 'waiting',
addPlayer: (id, name) => set((state) => ({
players: { ...state.players, [id]: { name, score: 0 } }
})),
updateScore: (playerId, score) => set((state) => ({
players: {
...state.players,
[playerId]: { ...state.players[playerId], score }
}
})),
startGame: () => set({ gameState: 'playing' }),
}),
{
namespace: 'multiplayer-game',
apiBaseUrl: process.env.HPKV_API_BASE_URL,
apiKey: process.env.HPKV_API_KEY, // Server-side only
}
)
);
// Usage in vanilla JS
gameStore.getState().addPlayer('player1', 'Alice');
gameStore.getState().startGame();
// Subscribe to changes
const unsubscribe = gameStore.subscribe((state) => {
console.log('Game state updated:', state);
updateGameUI(state);
});Node.js Server-Side
Server-side stores can use your API key directly for authentication (no token generation endpoint needed). When client and server stores share the same namespace, they automatically synchronize state in real-time:
// server-store.js
import { createStore } from 'zustand/vanilla';
import { multiplayer } from '@hpkv/zustand-multiplayer';
const serverStore = createStore()(
multiplayer(
(set) => ({
notifications: [],
addNotification: (message) => set((state) => ({
notifications: [...state.notifications, { message, timestamp: Date.now() }]
})),
}),
{
namespace: 'live-updates',
apiBaseUrl: process.env.HPKV_API_BASE_URL,
apiKey: process.env.HPKV_API_KEY,
}
)
);
// Event-driven updates (when something actually happens)
app.post('/webhook/payment', (req, res) => {
serverStore.getState().addNotification(`Payment received: $${req.body.amount}`);
res.json({ received: true });
});
app.post('/api/user-signup', (req, res) => {
serverStore.getState().addNotification(`New user: ${req.body.name}`);
res.json({ success: true });
});// client-store.js
export const useAppStore = create()(
multiplayer(
(set) => ({
notifications: [],
addNotification: (message) => set((state) => ({
notifications: [...state.notifications, { message, timestamp: Date.now() }]
})),
}),
{
namespace: 'live-updates', // Same namespace = shared state
apiBaseUrl: 'hpkv-api-base-url',
tokenGenerationUrl: '/api/generate-token',
}
)
);Advanced Features
Offline Conflict Resolution
When a client goes offline and comes back online, it may have missed updates from other clients. The conflict resolution system handles reconciling local pending changes with the current server state:
// store.js
export const useDocumentStore = create(
multiplayer(
(set) => ({
title: '',
content: '',
lastModified: null,
setTitle: (title) => set({ title, lastModified: Date.now() }),
setContent: (content) => set({ content, lastModified: Date.now() }),
}),
{
namespace: 'shared-document',
apiBaseUrl: 'hpkv-api-base-url',
tokenGenerationUrl: '/api/generate-token',
// Handle conflicts when reconnecting after being offline
onConflict: (conflicts) => {
console.log('Resolving conflicts after reconnection:', conflicts);
// Example: Smart merge for document content
const contentConflict = conflicts.find(c => c.field === 'content');
if (contentConflict) {
// Your local changes while offline
const localContent = contentConflict.pendingValue;
// Changes from other clients while you were offline
const remoteContent = contentConflict.remoteValue;
// Custom merge strategy
return {
strategy: 'merge',
mergedValues: {
content: mergeDocumentContent(localContent, remoteContent),
lastModified: Date.now()
}
};
}
// For other fields, prefer remote (server) version
return { strategy: 'keep-remote' };
}
}
)
);
function mergeDocumentContent(localContent, remoteContent) {
// Simple append strategy - you could implement more sophisticated merging
if (localContent === remoteContent) return localContent;
return `${remoteContent}\n\n--- Your offline changes ---\n${localContent}`;
}When conflicts occur:
1. Client goes offline - continues making local changes
2. Other clients make changes - updates are synced to server
3. Client comes back online - detects conflicts between local pending changes and current server state
4. Conflict resolution triggers - your onConflict handler decides how to merge
Available strategies:
keep-remote: Use the server state (default - safe choice)keep-local: Use your local changes (may overwrite others' work)merge: Custom merge withmergedValues(recommended for collaborative editing)
Selective Synchronization
Control what gets shared:
export const useUserStore = create(
multiplayer(
(set) => ({
theme: 'light',
preferences: {},
privateData: {},
setTheme: (theme) => set({ theme }),
updatePreferences: (prefs) => set({ preferences: prefs }),
}),
{
namespace: 'user-session',
apiBaseUrl: process.env.REACT_APP_HPKV_API_BASE_URL,
tokenGenerationUrl: '/api/generate-token',
// Only share theme preference, keep other data local
publishUpdatesFor: () => ['theme'],
subscribeToUpdatesFor: () => ['theme'],
}
)
);Connection Monitoring
Track connection health using reactive state:
import { useMyStore } from './store';
import { ConnectionState } from '@hpkv/websocket-client';
function ConnectionMonitor() {
const { multiplayer } = useMyStore((state) => state.multiplayer);
return (
<div>
<p>Connected: {multiplayer.connectionState === ConnectionState.CONNECTED ? 'Yes' : 'No'}</p>
</div>
);
}Manual Control
Take control when needed:
// components/AdminControls.js
function AdminControls() {
const { multiplayer } = useMyStore();
return (
<div>
<button onClick={() => multiplayer.hydrate()}>
Refresh from Server
</button>
<button onClick={() => multiplayer.clearStorage()}>
Clear All Data
</button>
<button onClick={() => multiplayer.disconnect()}>
Disconnect
</button>
<button onClick={() => multiplayer.connect()}>
Reconnect
</button>
</div>
);
}Core Concepts
Namespaces
Each store has a unique namespace that:
- Identifies your data in HPKV (keys are prefixed with
namespace:) - Enables collaboration - stores with the same namespace share data
- Provides isolation - different namespaces don't interfere with each other
Authentication
- Client-side: Use
tokenGenerationUrlpointing to your secure backend endpoint - Server-side: Use
apiKeydirectly (never expose in client code)
State Persistence
All published state changes are automatically:
- Persisted to HPKV for durability
- Synchronized across all connected clients in real-time
Configuration
Basic Options
{
namespace: 'my-app', // Required: unique identifier
apiBaseUrl: 'hpkv-api-base-url', // Required: your HPKV base URL
tokenGenerationUrl: '/api/token', // Required for client-side
apiKey: 'your-api-key', // Required for server-side
}Advanced Options
{
// Selective sync
publishUpdatesFor: () => ['field1', 'field2'],
subscribeToUpdatesFor: () => ['field1', 'field3'],
// Lifecycle hooks
onHydrate: (state) => console.log('Hydrated:', state),
onConflict: (conflicts) => ({ strategy: 'keep-remote' }),
// Performance & debugging
logLevel: LogLevel.INFO,
profiling: true,
retryConfig: {
maxRetries: 5,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2,
},
// Websocket connection tuning
clientConfig: {
maxReconnectAttempts: 10,
throttling: { enabled: true, rateLimit: 100 }
}
}TypeScript Support
Always use WithMultiplayer<T> wrapper for proper typing:
interface MyState {
count: number;
increment: () => void;
}
// ✅ Correct
export const useMyStore = create<WithMultiplayer<MyState>>()(
multiplayer(/* ... */)
);
// ❌ Incorrect - missing WithMultiplayer wrapper
export const useMyStore = create<MyState>()(
multiplayer(/* ... */)
);Documentation
- API Reference - Complete API documentation
- Token API Guide - Authentication setup
- How It Works - Technical deep dive
Examples Repository
Check out the examples/ directory for complete working applications:
- Next.js Todo App - Full-stack collaborative todo application
- Express Backend - Server-side store with REST API
Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
License
MIT - see LICENSE for details.
Need help? Check our documentation or open an issue.
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago