Skip to main content

Zustand

Create a Store

here's a basic store for an example Toast feature:

type ToastProps = {
id: string
type: string
props: object
}

export type ToastState = {
toasts: Array<ToastProps>
addToast: (toast: ToastProps) => void
removeToast: (id: string) => void
}

export const useToastStore = create<ToastState>((set) => ({
toasts: [],
addToast: (toast) => {
set((state) => ({
toasts: [...state.toasts, { ...toast, id: uid() }],
}))
},
removeToast: (id: string): void => {
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id),
}))
},
}))

Using the Toast Store

My code was initially this:

const { toasts } = useToastStore();

but I then changed it to this:

const toasts = useToastStore((state: ToastState) => state.toasts);

I don't have time to explain, here are some AI-generated notes:

AI Explanation

The difference between these two approaches lies in how they subscribe to and extract state from a Zustand store in a React component, affecting performance and re-render behavior.

const toasts = useToastStore((state: ToastState) => state.toasts);

This approach uses a selector function (state: ToastState) => state.toasts to subscribe only to the toasts slice of the store's state. The selector function allows the component to re-render only when the specific part of the state it subscribes to (toasts in this case) changes. This is more performant for components that only need to access a specific piece of the store, as it prevents unnecessary re-renders when other parts of the store's state change.

const { toasts } = useToastStore();

This approach destructures the toasts property directly from the store's entire state object returned by useToastStore(). It subscribes the component to the entire store state, meaning the component will re-render whenever any piece of the store's state changes, not just the toasts. While this is simpler and more straightforward, it can lead to unnecessary re-renders if the store contains multiple state slices and other slices change frequently.

Key Differences

Performance and Re-rendering: The selector approach (useToastStore((state) => state.toasts)) is more performant for components that don't need the entire store state because it limits re-renders to changes in the subscribed slice of state. The destructuring approach (const { toasts } = useToastStore()) can lead to unnecessary re-renders if other parts of the state change.

Subscription Scope: The selector approach explicitly defines the scope of the subscription to a specific part of the state, which can be beneficial for clarity and optimizing component updates. The destructuring approach subscribes to the entire state, which is simpler but less optimized.

Code Clarity and Maintenance: Using a selector can make it clearer that the component is only interested in a specific part of the state, potentially making the code easier to understand and maintain, especially in larger projects with complex state management.

Conclusion

Selecting which approach to use depends on your specific use case:

For components that only need to access a specific part of the store and you want to optimize for performance, use a selector function.

For components where you might need multiple pieces of the store or you prefer simplicity and the additional re-renders are not a concern, directly destructuring from the store might be more convenient.