Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on Zustand.
In our last lesson, we explored how to implement asynchronous actions to fetch and manage data from remote APIs. We saw how Zustand's simplicity allows for async/await syntax directly in your store actions, without needing any special middleware.
As you start building more features, each with its own state and async logic (e.g., fetching user profiles, managing a shopping cart, handling UI state), your single store file can become very large and difficult to manage. Today, we'll tackle this exact problem.
This lesson addresses the learning outcome: Organize a large store using the 'slice pattern' for improved scalability and maintainability. We will learn how to break down a monolithic store into smaller, modular, and feature-focused "slices," making your codebase cleaner, more organized, and easier to collaborate on.
1. The Problem: The Monolithic Store
Imagine an application with authentication, user profiles, and a product catalog. A single store file might look like this:
// A large, monolithic store - hard to maintain!
const useAppStore = create((set, get) => ({
// Auth state
token: null,
isAuthenticated: false,
// Auth actions
login: async (credentials) => { /* ... */ },
logout: () => { /* ... */ },
// Profile state
profile: null,
isLoadingProfile: false,
// Profile actions
fetchProfile: async () => { /* ... */ },
updateProfile: async (data) => { /* ... */ },
// Product state
products: [],
isLoadingProducts: false,
// Product actions
fetchProducts: async () => { /* ... */ },
// ...and so on
}));
As the application grows, this single create block becomes a bottleneck. It's hard to read, and multiple developers working on different features (like auth and products) are likely to cause merge conflicts in the same file.
The solution is to modularize the store by feature, which is what the slice pattern enables.

2. Implementing the Slice Pattern
The "slice pattern" isn't a special API in Zustand; it's a convention for organizing your code. The core idea is to define each part of your store in a separate function and then combine them into one final store.
Let's see how this is done using the official Zustand documentation.
The official Zustand documentation provides the canonical example of the slice pattern. It's the best place to start to understand the fundamental structure.
Please read the first section, 'Slicing the store into smaller stores'. Focus on how createFishSlice and createBearSlice are defined as simple functions. Then, observe how they are combined into useBoundStore using the spread operator (...).
Let's break down the key concepts from that example:
-
Slice Creator Function: A slice is just a function that accepts the
setandgetarguments (and theapiobject, which is less commonly used) and returns an object containing that slice's state and actions.export const createBearSlice = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), // ... }) -
Combining Slices: In your main store file, you import these slice creators and combine them inside the
createfunction. The(...a)syntax captures all arguments passed bycreate(which areset,get,api) into an arraya, and the spread syntax...apasses them along to each slice creator.import { create } from 'zustand' import { createBearSlice } from './bearSlice' import { createFishSlice } from './fishSlice' export const useBoundStore = create((...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), }))
This structure gives you clear separation. The logic for bears is in bearSlice.js, and the logic for fish is in fishSlice.js.
3. Using the Sliced Store in Components
A major benefit of this pattern is that from a component's perspective, nothing changes. You still interact with a single, unified store hook.
The documentation you just read also shows how to use the combined store. In a component, you simply import useBoundStore and select the state or actions you need, regardless of which slice they came from.
import { useBoundStore } from './stores/useBoundStore'
function BearCounter() {
// 'bears' comes from the bear slice
const bears = useBoundStore((state) => state.bears)
return <h1>{bears} Bears</h1>
}
function FishCounter() {
// 'fishes' comes from the fish slice
const fishes = useBoundStore((state) => state.fishes)
return <h1>{fishes} Fishes</h1>
}
The implementation is an internal detail; the component's API remains simple and consistent.
4. Cross-Slice Interactions
What if an action in one slice needs to affect another? For example, what if a bear eats a fish? The eatFish action in the bearSlice needs to decrease the fishes count in the fishSlice.
Zustand handles this gracefully because the set function's updater always receives the entire combined state.
Looking back at the createBearSlice from the documentation (6b1db):
export const createBearSlice = (set) => ({
bears: 0,
addBear: () => set((state) => ({ bears: state.bears + 1 })),
eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), // <-- Updates fish slice!
})
The eatFish action can directly set fishes because the state object it receives is the full state of the bound store.
For more complex interactions, especially calling an action from another slice, you can use the get() function.
The Zustand docs also show a pattern for composing actions from different slices.
Please read the section 'Updating multiple stores'. Notice how the createBearFishSlice uses get() to call actions defined in other slices (get().addBear() and get().addFish()).
This pattern is extremely powerful for creating complex workflows that span multiple domains of your state, while still keeping the source code for each domain neatly organized in its own file.
5. The Slice Pattern with TypeScript
For professional development, using TypeScript is standard practice. The slice pattern works beautifully with TypeScript, but it requires defining the types for each slice and the combined store.
Understanding Zustand: Real life example with slice pattern
This blog post provides a clear, practical example of implementing the slice pattern with TypeScript.
Please read the section 'Setting Up the Zustand Store with Slices'. Pay close attention to the use of StateCreator. This utility type from Zustand is used to type the slice creator function itself, ensuring that the arguments (set, get, api) and the return type are correct.
The key takeaways for TypeScript are:
- Define an
interfacefor each slice's state and actions (e.g.,ContactFormSlice). - Define a combined
interfacefor the entire store (e.g.,interface Store extends ContactFormSlice, AnotherSlice {}). - Use the
StateCreator<Store, [], [], ContactFormSlice>generic to type your slice creator function. This tells TypeScript that this function creates aContactFormSlicepart of the largerStore.
This approach provides full type safety and autocompletion across your entire store, even when it's split into many files.
Conclusion
You now have a powerful strategy for keeping your Zustand store organized, scalable, and maintainable as your React application grows. The slice pattern is a convention, not a rigid framework, giving you the flexibility to structure your state management in a way that best fits your project's architecture.
Key Takeaways:
- Monolithic stores are hard to maintain. The slice pattern solves this by organizing state logic by feature.
- Slices are creator functions. Each slice is a function that takes
(set, get)and returns an object of state and actions. - Combine slices with the spread operator. A single
create((...a) => ({ ...slice1(...a), ...slice2(...a) }))call brings everything together. - The component API is unchanged. Components interact with a single hook, unaware of the underlying slice structure.
- Cross-slice updates are possible. Use
set(state => ...)to update other slices' state orget().otherAction()to call actions from other slices.
In our next lesson, we will explore middleware. We'll see how middleware can wrap your store to add powerful, cross-cutting functionalities like connecting to the Redux DevTools for debugging (devtools) and persisting state to localStorage (persist). You've already had a small preview of how middleware is applied to a combined store, and next time we'll dive deep into how it works.