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
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>
}
ContextAPI Layer
Initializing Context API
// product-store.tsx
'use client'
import { createContext} from 'react';
export const ProductStoreContext = createContext(null);
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>
);
};
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>
);
};
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);
}
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 });
},
});
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),
}));
Alright, Now you've a very solid zustand store architecture to start with...
Thanks for reading! 💖
Top comments (0)