Zustand Slices Utility
zustand-slices is a utility library designed to introduce an opinionated, TypeScript-friendly slice pattern for Zustand, a minimalist global state management library. As of version 0.4.0, it provides `createSlice` and `withSlices` helpers to structure Zustand stores into modular, reusable "slices." This addresses the complexities often encountered when attempting to implement such patterns with strong TypeScript typing, particularly when following the official Zustand documentation's manual approach. The library is actively developed, with its primary maintainer frequently tweeting about updates and examples, suggesting a consistent, though not strictly scheduled, release cadence focusing on refinement and new features. Its key differentiator is its explicit support for type inference and clean separation of concerns within a Zustand store, leveraging immutable updates via Immer, which is a peer dependency.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'produce')
cause The Immer peer dependency is not installed, but zustand-slices expects it for immutable state updates.fixInstall Immer: `npm install immer` or `yarn add immer`. -
TS2345: Argument of type '...' is not assignable to parameter of type 'StateCreator<...>'
cause TypeScript type inference failed, often due to incorrect generic parameters for `createSlice` or `create` when combining complex state shapes or applying middleware.fixCarefully review your `StateCreator` generics and ensure consistency across slices and the main store. Explicitly typing `createSlice<T>()(...)` can help guide inference. -
Property 'someProperty' does not exist on type '...' (when accessing state)
cause The state property you are trying to access does not exist on the combined store's type, or there's a typo. This can happen if a slice was not correctly combined or if type inference missed a property.fixVerify that all slices are correctly passed to `withSlices` and that the property name matches the slice's defined state. Ensure your TypeScript configuration is strict enough to catch such issues.
Warnings
- gotcha Immer is a required peer dependency for `zustand-slices` to function correctly with immutable updates, but it is not automatically installed. Failing to install Immer will lead to runtime errors or unexpected state mutations if slice actions expect Immer's draft mechanism.
- gotcha When combining multiple slices with `withSlices`, be cautious of action name collisions (e.g., two slices both defining a `reset` action). The last slice combined will overwrite previous actions with the same name, leading to unexpected behavior.
- gotcha Directly mutating state within slice actions without leveraging Immer's draft mechanism (when Immer is installed) or explicitly returning a new state object can lead to un-tracked changes and inconsistent state, as Zustand expects immutable updates.
- gotcha TypeScript inference can sometimes be tricky with complex slice patterns and middleware in Zustand. While `zustand-slices` aims to improve this, incorrect generic types or middleware application can still lead to type errors.
Install
-
npm install zustand-slices -
yarn add zustand-slices -
pnpm add zustand-slices
Imports
- createSlice
const createSlice = require('zustand-slices').createSlice;import { createSlice } from 'zustand-slices'; - withSlices
const { withSlices } = require('zustand-slices');import { withSlices } from 'zustand-slices'; - create
import { create } from 'zustand-slices';import { create } from 'zustand';
Quickstart
import { create } from 'zustand';
import { createSlice, withSlices } from 'zustand-slices';
interface CountState {
count: number;
inc: () => void;
resetCount: () => void;
}
interface TextState {
text: string;
updateText: (newText: string) => void;
resetText: () => void;
}
const countSlice = createSlice<CountState>()({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
resetCount: () => () => 0,
},
});
const textSlice = createSlice<TextState>()({
name: 'text',
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
resetText: () => () => 'Hello',
},
});
// Combine slices and create the store
const useStore = create(withSlices(countSlice, textSlice));
// Example component usage (requires React environment)
function MyComponent() {
const count = useStore((state) => state.count);
const text = useStore((state) => state.text);
// Destructuring actions from getState() to ensure referential stability
const { inc, updateText, resetCount, resetText } = useStore.getState();
return (
<>
<p>
Count: {count}
<button type="button" onClick={inc}>
+1
</button>
</p>
<p>
<input value={text} onChange={(e) => updateText(e.target.value)} />
</p>
<p>
<button type="button" onClick={resetCount}>
Reset Count
</button>
<button type="button" onClick={resetText}>
Reset Text
</button>
</p>
</>
);
}
// To use MyComponent, render it within a React application.
// For example, in a simple setup:
// import React from 'react';
// import ReactDOM from 'react-dom/client';
// const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<MyComponent />);