@okyrychenko-dev/react-zustand-toolkit
v0.2.0
Published
A powerful toolkit for creating type-safe Zustand stores with automatic shallow comparison, provider patterns, and smart context resolution
Maintainers
Readme
@okyrychenko-dev/react-zustand-toolkit
A powerful toolkit for creating type-safe Zustand stores with automatic shallow comparison, provider patterns, and smart context resolution.
Features
- Automatic Shallow Comparison: Built-in shallow equality checks for all selectors
- Provider Pattern: Create isolated store instances for SSR, testing, and micro-frontends
- Smart Resolution: Hooks that automatically resolve between global and context stores
- Type-Safe: Full TypeScript support with comprehensive type inference
- Zero Configuration: Works out of the box with sensible defaults
- DevTools Integration: Redux DevTools support for debugging
- Middleware Support: Compatible with all Zustand middleware
Installation
npm install @okyrychenko-dev/react-zustand-toolkit zustand
# or
yarn add @okyrychenko-dev/react-zustand-toolkit zustand
# or
pnpm add @okyrychenko-dev/react-zustand-toolkit zustandQuick Start
Basic Store with Toolkit
import { createStoreToolkit } from "@okyrychenko-dev/react-zustand-toolkit";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
}
const counterToolkit = createStoreToolkit<CounterStore>(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}),
{ name: "Counter" }
);
// Export hooks
export const { useStore: useCounter, useResolvedStoreWithSelector: useCounterResolved } =
counterToolkit;
// Create provider
export const { Provider: CounterProvider } = counterToolkit.createProvider();Use in Components
function Counter() {
// Works both inside and outside provider
const count = useCounterResolved((state) => state.count);
const increment = useCounterResolved((state) => state.increment);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}Wrap with Provider (Optional)
function App() {
return (
<CounterProvider enableDevtools={true}>
<Counter />
</CounterProvider>
);
}API Reference
createStoreToolkit<TState>(storeCreator, options?)
Creates a complete toolkit with global store, provider factory, and resolution hooks.
Parameters:
storeCreator: Function that creates store state and actionsoptions.name: Optional name for DevTools and Provider
Returns:
useStore: Hook for global store with shallow comparisonuseStoreApi: Direct access to store APIcreateProvider(): Factory to create provideruseResolvedStore(): Returns store API (context or global)useResolvedStoreWithSelector(): Smart hook with selector support
createShallowStore<TState>(storeCreator)
Creates a Zustand store with automatic shallow comparison.
Parameters:
storeCreator: Function that creates store state and actions
Returns:
useStore: Hook with shallow comparisonuseStoreApi: Direct access to store API
const { useStore, useStoreApi } = createShallowStore<MyStore>((set) => ({
// your store implementation
}));createStoreProvider<TState>(storeCreator, contextName?)
Creates a React Context provider for isolated store instances.
Parameters:
storeCreator: Function that creates store state and actionscontextName: Optional name for debugging
Returns:
Provider: React component to wrap your app/subtreeuseContext(): Hook to access store from context (throws if outside provider)useContextStore(): Hook to access store with selector (throws if outside provider)useOptionalContext(): Hook to access store from context (returns null if outside provider)useIsInsideProvider(): Check if inside provider
const { Provider, useContextStore } = createStoreProvider<MyStore>((set) => ({
// your store implementation
}));Usage Patterns
Pattern 1: Global Singleton Store
import { createShallowStore } from "@okyrychenko-dev/react-zustand-toolkit";
const { useStore: useGlobalStore } = createShallowStore<MyStore>((set) => ({
// implementation
}));
// Use anywhere
const data = useGlobalStore((state) => state.data);Pattern 2: Isolated Instances with Provider
import { createStoreProvider } from "@okyrychenko-dev/react-zustand-toolkit";
const { Provider, useContextStore } = createStoreProvider<MyStore>((set) => ({
// implementation
}));
// Wrap your app
<Provider>
<App />
</Provider>
// Use in components
const data = useContextStore((state) => state.data);Pattern 3: Smart Resolution (Recommended)
import { createStoreToolkit } from "@okyrychenko-dev/react-zustand-toolkit";
const toolkit = createStoreToolkit<MyStore>((set) => ({
// implementation
}));
export const { useResolvedStoreWithSelector: useMyStore } = toolkit;
export const { Provider: MyStoreProvider } = toolkit.createProvider();
// Works without provider (uses global store)
const data = useMyStore((state) => state.data);
// Works with provider (uses isolated store)
<MyStoreProvider>
<Component /> {/* Same hook works here too */}
</MyStoreProvider>Pattern 4: With Middleware (DevTools)
import { devtools } from "zustand/middleware";
import { createShallowStore } from "@okyrychenko-dev/react-zustand-toolkit";
const { useStore } = createShallowStore<MyStore, [["zustand/devtools", never]]>(
devtools(
(set) => ({
// implementation
}),
{ name: "MyStore" }
)
);Use Cases
Server-Side Rendering (SSR)
Each request gets its own isolated store instance:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<MyStoreProvider>
{children}
</MyStoreProvider>
);
}Testing
No need for beforeEach cleanup - each test gets a fresh instance:
function wrapper({ children }) {
return <MyStoreProvider>{children}</MyStoreProvider>;
}
test("my test", () => {
const { result } = renderHook(() => useMyStore(), { wrapper });
// Fresh store for this test
});Micro-Frontends
Each micro-frontend gets its own isolated state:
// MicroApp1
<MyStoreProvider>
<MicroApp1 />
</MyStoreProvider>
// MicroApp2
<MyStoreProvider>
<MicroApp2 />
</MyStoreProvider>Store Initialization with onStoreCreate
Use onStoreCreate callback to initialize store after creation (e.g., register middlewares):
interface AppStore {
// ... state
registerMiddleware: (name: string, middleware: Middleware) => void;
}
const { Provider } = createStoreProvider<AppStore>((set) => ({
// ... implementation
}));
// Initialize store with middlewares
<Provider
onStoreCreate={(store) => {
store.getState().registerMiddleware('logger', loggerMiddleware);
store.getState().registerMiddleware('analytics', analyticsMiddleware);
}}
>
<App />
</Provider>TypeScript
Full type inference and type safety:
interface UserStore {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const toolkit = createStoreToolkit<UserStore>((set) => ({
user: null,
login: async (credentials) => {
const user = await api.login(credentials);
set({ user });
},
logout: () => set({ user: null }),
}));
// Type-safe selectors
const user = toolkit.useStore((state) => state.user); // User | null
const login = toolkit.useStore((state) => state.login); // (credentials: Credentials) => Promise<void>Development
# Install dependencies
npm install
# Run tests
npm run test
# Run tests with coverage
npm run test:coverage
# Build the package
npm run build
# Type checking
npm run typecheck
# Lint code
npm run lint
# Fix lint errors
npm run lint:fix
# Format code
npm run format
# Watch mode for development
npm run devContributing
Contributions are welcome! Please ensure:
- All tests pass (
npm run test) - Code is properly typed (
npm run typecheck) - Linting passes (
npm run lint) - Code is formatted (
npm run format)
License
MIT © Oleksii Kyrychenko
