DEV Community

Vikash Kumar
Vikash Kumar

Posted on

How to structure slices with in a Zustand Store

States in a react application plays the role of soul in the application.

A small react application can handle states via context api for the simplicity.
But as the application grows it needs a better state management system.

Zustand Store

I am not going to talk about what is zustand ?
How to configure it with a react ( or next ) application ?
Also not going to talk about the installation process... 😴 ( Skip the boring stuff )

Here is the actual magical code, I am gonna share... 🪄

src/
└── stores/
    └── product/
        ├── index.ts
        ├── product-store.tsx
        └── slices/
            ├── create-owner-slice.ts
            └── create-product-slice.ts
Enter fullscreen mode Exit fullscreen mode

In this plan, I am going to wrap the store within context api layer for SSR data injection via server components.

// page.tsx

export default async function Page({ params }) {
  const { id: productId } = await params;
  const response = await fetch('https://api.shop.com/productId');

  if (!response.ok) {
    throw new Error('Failed to fetch product');
  }

  const product: Product = await response.json();

  return (
    <ProductStoreProvider initialData={Product}>
      <ProductView />
    </ProductStoreProvider>
}
Enter fullscreen mode Exit fullscreen mode

ContextAPI Layer

Initializing Context API

// product-store.tsx

'use client'
import { createContext} from 'react';

export const ProductStoreContext = createContext(null);
Enter fullscreen mode Exit fullscreen mode

Setup the Provider layer with InitialData passed by server component.

// product-store.tsx

import { useRef, PropsWithChildren } from 'react';

interface ProductStoreProviderProps extends PropsWithChildren {
  initialData: Product;
}

export const ProductStoreProvider = ({ children, initialData }: ProductStoreProviderProps) => {
  const ref = useRef(null);

  return (
    <ProductStoreContext.Provider value={ref.current}>{children}</ProductStoreContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

In this state, We've built the shell of Zustand Store. 😋

Now lets inject the initialData using a factory function to create zustand store

// product-store.tsx

import { create } from 'zustand';
import { useRef, PropsWithChildren } from 'react';

const createProductStore = (initialData: Product) =>
  create<ProductStore>((set, get) => ({
    // slices...
  }));

export const ProductStoreProvider = ({ children, initialData }: ProductStoreProviderProps) => {
  const ref = useRef(null);
  if (!ref.current) {
    ref.current = createProductStore(initialData);
  }

  return (
    <ProductStoreContext.Provider value={ref.current}>{children}</ProductStoreContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

finally, a hook() 🪝 to retrive data from the store

// product-store.tsx

export function useProductStore<T>(selector: (state: ProductStore) => T): T {
  const context = useContext(ProductStoreContext);
  if (!context) {
    throw new Error('useProductStore must be used within ProductStoreProvider');
  }
  return context(selector);
}
Enter fullscreen mode Exit fullscreen mode

Why did I wrap the store with context api layer?

The cleanup strategy of a zustand store now entirely eliminated.
No more worries to clean up the store after unmounting. 😎🚀

Slices

Here is the most common slice demonstration,
You can refer to build a complex slice ( I mean simple slice 🤭 )

// create-product-slice.ts

import { StoreApi } from 'zustand';
import { ProductStore } from '../product-store' 

type SetState = StoreApi<ProductStore>['setState'];
type GetState = StoreApi<ProductStore>['getState'];

type ProductData = Pick<ProductSlice, 'name' | 'price' | 'category' | 'labels'>;

export interface ProductSlice {
  name: string | null;
  price: number;
  category: 'Steel' | 'Wooden' | 'Plastic' | null;
  labels: string[];
  onUpdate(data: { key: keyof ProductData; value: ProductData[keyof ProductData] }): void;
}

const createProductSlice = (set: SetState, get: GetState, initialData: Product): ProductSlice => ({
  name: initialData.name ?? null,
  price: initialData.price ?? 0,
  category: initialData.category ?? null,
  labels: initialData.labels ?? [],

  onUpdate: (data) => {
    const { key, value } = data;
    set({ [key]: value });
  },
});
Enter fullscreen mode Exit fullscreen mode

Then update the product-store.tsx a bit to reflect the current changes

// product-store.tsx

import { createProductSlice } from './slices/create-product-slice'

type ProductStore = ProductSlice;
// type ProductStore = OwnerSlice & ProductSlice; // Add more slices here 👈

const createProductStore = (initialData: Product) =>
  create<ProductStore>((set, get) => ({
    ...createProductSlice(set, get, initialData),
  }));
Enter fullscreen mode Exit fullscreen mode

Alright, Now you've a very solid zustand store architecture to start with...

Thanks for reading! 💖

Top comments (0)