# Tooling and Ecosystem Standards for Zustand
This document outlines the standards for tooling and ecosystem components when developing applications using Zustand. It provides guidelines for selecting and integrating libraries, tools, and extensions to enhance development workflows, improve code quality, and optimize performance.
## 1. Recommended Libraries and Tools
### 1.1. Devtools Integration
**Do This:** Integrate "zustand/middleware"'s "devtools" middleware for enhanced debugging capabilities.
**Don't Do This:** Skip devtools integration, especially in development environments. This makes debugging significantly harder.
**Why:** Devtools provide a time-travel debugging experience, allowing developers to inspect state changes and identify issues more efficiently.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})));
export default useStore;
"""
**Anti-Pattern:** Manually logging state changes. Devtools provide a structured and interactive way to observe these changes. Also, avoid leaving "devtools" enabled in production builds.
**Technology-Specific Details:** The "devtools" middleware connects to the Redux DevTools extension (available for Chrome, Firefox, and Edge). Ensure the extension is installed and enabled in your browser.
### 1.2. Persist Middleware
**Do This:** Utilize "zustand/middleware"'s "persist" middleware for persisting store data to local storage or other storage mechanisms.
**Don't Do This:** Implement custom persistence solutions. This increases the risk of errors and inconsistencies.
**Why:** Persistence ensures that application state is preserved across sessions, providing a better user experience.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(persist((set) => ({
bears: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
}), {
name: 'bear-storage', // unique name
getStorage: () => localStorage, // (optional) by default, 'localStorage' is used
}));
export default useStore;
"""
**Anti-Pattern:** Storing sensitive data in local storage without encryption. Consider using "redux-persist-transform-encrypt" or similar libraries to encrypt data. Also, avoid storing large amounts of data in local storage that could impact performance.
**Technology-Specific Details:** The "persist" middleware allows customization of the storage mechanism (e.g., "localStorage", "sessionStorage", cookies, IndexedDB). Provide a custom "getStorage" function to use alternative storage solutions. Be mindful of storage limits.
### 1.3. Immer Integration
**Do This:** Use "immer" to simplify immutable state updates, especially for complex state objects. Zustand itself doesn't require Immer, but its integration often leads to cleaner code. Consider using "set(produce(state => { ... }))" with Zustand, it already exports the produce function.
**Don't Do This:** Manually create new state objects for every update when dealing with deeply nested state.
**Why:** Immer allows you to work with mutable drafts of your state, which are then automatically converted to immutable updates, reducing boilerplate and improving code readability.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { produce } from 'immer';
const useStore = create((set) => ({
user: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'Anytown',
},
},
updateCity: (newCity) => set(produce((state) => {
state.user.address.city = newCity;
})),
}));
export default useStore;
"""
**Anti-Pattern:** Overusing Immer for simple state updates. For simple updates like incrementing a counter, direct state updates are more efficient.
**Technology-Specific Details:** Ensure "immer" is installed as a project dependency ("npm install immer" or "yarn add immer").
### 1.4. Zustand Selectors with "useRef"
**Do This:** Use selectors for derived state, and memoize them effectively using "useRef" to prevent unnecessary re-renders.
**Don't Do This:** Perform complex calculations directly within the component's render function. This can lead to performance issues.
**Why:** Selectors allow you to efficiently derive state from the store and only re-render components when the derived state changes. "useRef" provides a way to memoize the result of a selector, further optimizing performance.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { useRef } from 'react';
import { shallow } from 'zustand/shallow';
const useStore = create((set) => ({
items: [{ id: 1, price: 10 }, { id: 2, price: 20 }],
discount: 0.1,
setDiscount: (discount) => set({ discount }),
}));
const useTotal = () => {
const totalRef = useRef<(state: any) => number | null>(null);
if (!totalRef.current) {
totalRef.current = (state) => {
if (!state.items || state.items.length === 0) return null;
const subtotal = state.items.reduce((acc, item) => acc + item.price, 0);
return subtotal * (1 - state.discount);
}
}
return useStore(totalRef.current, shallow);
}
function MyComponent() {
const total = useTotal();
return (
Total: {total}
);
}
"""
**Anti-Pattern:** Creating new selector objects on every render. This defeats the purpose of memoization.
**Technology-Specific Details:** The "shallow" comparison function from "zustand/shallow" is helpful for preventing re-renders when only a nested property within the state object changes.
### 1.5 Computed Properties with Selectors and "useMemo"
**Do This:** Utilize "useMemo" in conjunction with Zustand selectors for complex computed properties to optimize performance and prevent unnecessary recalculations.
**Don't Do This:** Perform expensive computations directly within your components without memoization. This will cause performance issues, especially with frequently updating stores. Avoid creating functions inside the components, this will break the optimization.
**Why:** Memoization ensures that computed properties are only recalculated when their dependencies change, improving component rendering performance. This approach leverages React's "useMemo" hook to cache the result of a computation, making it efficient and performant.
**Code Example:**
"""jsx
import { create } from 'zustand';
import { useMemo } from 'react';
const useStore = create((set) => ({
items: [
{ id: 1, name: 'Apple', price: 1 },
{ id: 2, name: 'Banana', price: 0.5 },
{ id: 3, name: 'Orange', price: 0.75 },
],
taxRate: 0.1,
setTaxRate: (newRate) => set({ taxRate: newRate }),
}));
function useCalculatedTotal() {
const items = useStore(state => state.items);
const taxRate = useStore(state => state.taxRate);
const calculatedTotal = useMemo(() => {
console.log('Recalculating total'); // For demonstration
const subtotal = items.reduce((acc, item) => acc + item.price, 0);
const taxAmount = subtotal * taxRate;
return subtotal + taxAmount;
}, [items, taxRate]);
return calculatedTotal;
}
function ShoppingCart() {
const total = useCalculatedTotal();
const setTaxRate = useStore(state => state.setTaxRate);
return (
<p>Total: ${total.toFixed(2)}</p>
setTaxRate(0.2)}>Increase Tax Rate
);
}
export default ShoppingCart;
"""
**Anti-Pattern:** Forgetting to include all dependencies in the "useMemo" dependency array. This can lead to stale values and incorrect computations. Also, recalculation will always happen on initial render.
**Technology-Specific Details:** This approach is particularly useful when the calculation involves complex logic or large datasets. Using "useMemo" ensures that the calculation is only performed when necessary, preventing performance bottlenecks.
### 1.6. Immer Integration with Redux DevTools
**Do This:** Combine "immer" and Redux DevTools for powerful debugging and state management when using Zustand
**Don't Do This:** Avoid using DevTools when using Immer with Zustand. This leads to difficulties in debugging state transformation.
**Why:** This combination allows you to visualize each state mutation with Immer while leveraging the time-travel debugging features of the DevTools. It helps track the precise evolution of the state, greatly simplifying debugging complex applications.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware';
const useStore = create(devtools(immer((set) => ({
counter: 0,
increment: () => set((state) => {
state.counter += 1; // Immer allows direct mutation here
}),
decrement: () => set((state) => {
state.counter -= 1; // Immer handles immutability under the hood
}),
}))));
export default useStore;
"""
**Anti-Pattern:** Applying Immer without using DevTools. This deprives you of the ability to visualize the exact mutations performed via Immer, increasing debugging complexity.
**Technology-Specific Details:** Immer enhances the debugging experience in Redux DevTools, enabling developers to inspect the state before and after each mutation step, making it easier to pinpoint the source of issues
### 1.7 Testing with Zustand
**Do This:** Unit test your store logic thoroughly, mocking dependencies as necessary. Zustand stores are highly testable due to their simple, functional nature.
**Don't Do This:** Neglect store testing. Untested stores can lead to unexpected behavior and difficult-to-debug issues. Also, avoid testing implementation details. Focus on testing the public API of your store (actions and selectors).
**Why:** Testing ensures the correctness and reliability of your state management logic.
**Code Example (using Jest and @testing-library/react-hooks):**
"""javascript
import { create } from 'zustand';
import { act, renderHook } from '@testing-library/react-hooks';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
describe('useStore', () => {
it('should initialize count to 0', () => {
const { result } = renderHook(() => useStore());
expect(result.current.count).toBe(0);
});
it('should increment count when increment is called', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count when decrement is called', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
});
"""
**Anti-Pattern:** Testing the internal implementation of the store. Tests should focus on the public API (actions and selectors). Also, avoid tightly coupling tests to specific store implementation details.
**Technology-Specific Details:** Use "@testing-library/react-hooks" for testing Zustand stores in isolation, without rendering React components. "act" is crucial for wrapping state updates to ensure they are properly batched. Consider using Jest for mocking and assertions.
### 1.8 Zustand with TypeScript
**Do This:** Leverage TypeScript to define store state, actions, and selectors with strong types. This improves code maintainability and reduces runtime errors.
**Don't Do This:** Skip TypeScript usage in Zustand stores, especially for complex applications. This increases the risk of type-related errors and reduces code clarity.
**Why:** TypeScript provides static type checking, which helps catch errors early in the development process.
**Code Example:**
"""typescript
import { create } from 'zustand';
interface BearState {
bears: number;
increaseBears: () => void;
}
const useBearStore = create((set) => ({
bears: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
}));
export default useBearStore;
"""
**Anti-Pattern:** Using "any" type for store state. This defeats the purpose of using TypeScript. Provide specific type definitions for your state and actions.
**Technology-Specific Details:** Define interfaces or types for your state and actions. Use generic types to create reusable store definitions.
### 1.9 Zustand and React Server Components (RSCs)
**Do This**: Exercise extreme caution when mixing Zustand with React Server Components, especially in Next.js 13 and above. Understand the limitations described in the Zustand GitHub repository (see #2200). Prefer alternative data fetching and state management strategies for server-side data.
**Don't Do This**: Directly initialize or mutate Zustand stores within React Server Components unless you fully understand the implications for data privacy and potential bugs related to shared server state.
**Why**: Zustand is primarily designed for client-side state management. Incorrectly using it within RSCs can lead to unexpected behavior, data leakage between users, and difficulties in debugging due to the server-side execution context.
**Code Example (Illustrating a potential problem - AVOID THIS PATTERN unless you have a deep understanding of the consequences):**
"""javascript
// This is an ANTI-PATTERN for Next.js 13+ Server Components!
// Use with EXTREME CAUTION!
// NOT RECOMMENDED
import { create } from 'zustand';
const useStore = create((set) => ({
serverData: 'Initial Value',
updateServerData: (newValue) => set({ serverData: newValue }),
}));
//This component runs on the server! The store is shared across requests!
export default function MyServerComponent() {
const data = useStore((state) => state.serverData);
const updateData = useStore((state) => state.updateServerData);
return (
<p>Server Data: {data}</p>
updateData('New Value')}>Update Data
);
}
"""
**Anti-Pattern**: Assuming that Zustand will behave identically in RSCs as it does in client components. The server-side environment introduces complexities related to state persistence and sharing across requests. Relying on client-side initialization patterns will likely break.
**Technology-Specific Details**: In Next.js 13+, consider using server actions, route handlers, and alternative state management solutions like React Context with data fetched from a database or API when working with server components. Isolate client-side state to client components using the "'use client'" directive and initialize Zustand stores there. Thoroughly test any integration of Zustand with RSCs in a production-like environment.
## 2. Code Formatting and Linting
### 2.1. Consistent Code Style
**Do This:** Use a code formatter like Prettier to enforce consistent code style across the project.
**Don't Do This:** Rely on manual code formatting. This can lead to inconsistencies and increase code review time.
**Why:** Consistent code style improves code readability and maintainability.
**Code Example (.prettierrc.js):**
"""javascript
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};
"""
**Anti-Pattern:** Inconsistent indentation, spacing, and line breaks.
**Technology-Specific Details:** Integrate Prettier with your editor for automatic code formatting on save. Use a linter like ESLint to enforce code quality rules and catch potential errors.
### 2.2. ESLint Configuration
**Do This:** Configure ESLint with recommended rules for React and JavaScript/TypeScript. Consider using the "eslint-plugin-react-hooks" plugin to enforce rules of hooks.
**Don't Do This:** Disable important ESLint rules. While customization is possible, avoid disabling rules that enforce best practices.
**Why:** ESLint helps identify potential issues in your code and enforce coding standards.
**Code Example (.eslintrc.js):**
"""javascript
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['react', '@typescript-eslint'],
rules: {
// Add or override rules here
},
settings: {
react: {
version: 'detect',
},
},
};
"""
**Anti-Pattern:** Ignoring ESLint warnings and errors. Address these issues to improve code quality.
**Technology-Specific Details:** Integrate ESLint with your editor for real-time code analysis. Use a pre-commit hook (e.g., with Husky and lint-staged) to ensure that all code passes linting checks before being committed.
## 3. Performance Optimization
### 3.1. Minimizing Re-renders
**Do This:** Use selectors and "shallow" comparison to prevent unnecessary re-renders.
**Don't Do This:** Connect components directly to the store's entire state. This causes re-renders even when only a small part of the state changes.
**Why:** Reducing re-renders improves application performance and responsiveness.
**Code Example:** See section 1.4 for example usage
**Anti-Pattern:** Passing the entire store state as props to child components. Use selectors to pass only the necessary data.
### 3.2. Batching Updates
**Do This:** Utilize "React.batch" or equivalent mechanisms to batch multiple state updates into a single re-render.
**Don't Do This:** Trigger multiple state updates within a short time period without batching. This can lead to performance issues.
**Why:** Batching reduces the number of re-renders, improving performance.
**Code Example:**
"""javascript
import { unstable_batchedUpdates } from 'react-dom'; // react-dom version >= 18
const updateMultipleValues = () => {
unstable_batchedUpdates(() => {
useStore.setState({ value1: 'new value 1' });
useStore.setState({ value2: 'new value 2' });
});
};
"""
**Anti-Pattern:** Manually managing update queues without leveraging "unstable_batchedUpdates".
**Technology-Specific Details:** "unstable_batchedUpdates" is available in "react-dom". Consider using "ReactDOM.unstable_batchedUpdates" if not using React 18 or later.
## 4. Security Considerations
### 4.1. Data Sanitization
**Do This:** Sanitize user input before storing it in the store to prevent XSS attacks.
**Don't Do This:** Store unsanitized user input directly in the store. This can create security vulnerabilities.
**Why:** Sanitization helps prevent malicious code from being injected into your application.
**Code Example:**
"""javascript
import { create } from 'zustand';
import DOMPurify from 'dompurify';
const useStore = create((set) => ({
userInput: '',
updateUserInput: (input) => set({ userInput: DOMPurify.sanitize(input) }),
}));
export default useStore;
"""
**Anti-Pattern:** Storing sensitive information without proper encryption. Also, passing the whole state object without validation.
### 4.2. Preventing State Tampering
**Do This:** Protect your store from unauthorized modifications. Consider using middleware to validate state updates.
**Don't Do This:** Allow arbitrary modifications to the store without validation. This can compromise the integrity of your application.
**Why:** Protecting the store from unauthorized modifications ensures that your application behaves as expected.
Remember to always refer to the [official Zustand documentation](https://github.com/pmndrs/zustand) for the most up-to-date information and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for Zustand This document outlines the core architecture standards for Zustand. It is designed to guide developers in creating maintainable, performant, and scalable applications using Zustand as the state management solution. It focuses on fundamental architectural patterns, project structure, and organization principles specifically applied to Zustand within the context of modern JavaScript and React development. ## 1. Project Structure and Organization A well-organized project structure is crucial for maintainability and scalability. When using Zustand, carefully consider how you structure your files, especially as the application grows. ### 1.1 Standard: Feature-Based Organization **Do This:** Organize your project based on features or modules, rather than technology (e.g., components, actions, reducers). Each feature should have its own directory containing all related files, including Zustand stores, components, hooks, and tests. **Don't Do This:** Avoid a flat project structure or organizing files solely by technology type (e.g., all stores in a "/stores" directory, all components in a "/components" directory). **Why:** Feature-based organization improves code discoverability, reduces dependencies between features, and simplifies refactoring. **Example:** """ src/ ├── features/ │ ├── auth/ │ │ ├── authStore.js │ │ ├── components/ │ │ │ ├── Login.jsx │ │ │ └── Logout.jsx │ │ ├── hooks/ │ │ │ └── useAuth.js │ │ └── tests/ │ │ └── authStore.test.js │ ├── dashboard/ │ │ ├── dashboardStore.js │ │ ├── components/ │ │ │ └── Dashboard.jsx │ │ └── ... │ └── ... ├── App.jsx └── ... """ ### 1.2 Standard: Single Store per Feature (Generally) **Do This:** Aim for one primary store per feature. This helps encapsulate the state logic specific to that feature. **Don't Do This:** Creating a monolithic store that manages the entire application state, or excessively fragmenting the state into numerous, overly specific stores, particularly if they have tightly coupled dependencies. **Why:** A single store per feature promotes modularity and reduces the risk of naming conflicts and unintended side effects. Overly granular stores can lead to prop drilling and increased complexity in state synchronization. **Caveat:** There might be cases where multiple stores within a feature are justified (e.g., complex forms with isolated sections, truly independent sub-features). Use your judgment. **Example:** """javascript // src/features/auth/authStore.js import { create } from 'zustand'; const useAuthStore = create((set) => ({ user: null, isAuthenticated: false, login: async (credentials) => { // ... login logic set({ user: { name: 'John Doe' }, isAuthenticated: true }); }, logout: () => set({ user: null, isAuthenticated: false }), })); export default useAuthStore; // src/features/dashboard/dashboardStore.js import { create } from 'zustand'; const useDashboardStore = create((set) => ({ data: [], isLoading: false, fetchData: async () => { set({ isLoading: true }); // ... fetch data logic set({ data: [{ id: 1, name: 'Item 1' }], isLoading: false }); }, })); export default useDashboardStore; """ ### 1.3 Standard: Store Location and Access **Do This:** Place each store file within its respective feature directory. Access stores using custom hooks within the feature's components. **Don't Do This:** Import stores directly into components outside the feature. Avoid placing all stores in a generic "/stores" directory, breaking feature encapsulation. **Why:** This pattern reinforces modularity, making it clear which components depend on which stores and simplifying refactoring. **Example:** """javascript // src/features/auth/hooks/useAuth.js import useAuthStore from '../authStore'; const useAuth = () => { const { user, isAuthenticated, login, logout } = useAuthStore(); return { user, isAuthenticated, login, logout }; }; export default useAuth; // src/features/auth/components/Login.jsx import useAuth from '../hooks/useAuth'; const Login = () => { const { login } = useAuth(); const handleSubmit = (e) => { e.preventDefault(); // ... get credentials from form login({ username: 'test', password: 'password' }); }; return ( <form onSubmit={handleSubmit}> {/* ... form elements */} </form> ); }; export default Login; """ ### 1.4 Standard: Clear Naming Conventions **Do This:** Use consistent and descriptive names for stores and selectors. Prefix store hooks with "use" (e.g., "useAuthStore", "useDashboardStore"). Name selectors based on the data they retrieve (e.g., "selectUserName", "selectIsLoading"). **Don't Do This:** Using generic or ambiguous names that don't clearly indicate the store's purpose or the selector's return value (e.g., "dataStore", "getData"). **Why:** Clear naming improves code readability and makes it easier to understand the purpose of each store and selector. **Example:** """javascript // Good naming const useProductsStore = create((set) => ({ products: [], isLoading: false, fetchProducts: async () => { set({ isLoading: true }); // ... fetch products set({ products: /* fetched products */, isLoading: false }); }, selectProducts: (state) => state.products, selectIsLoading: (state) => state.isLoading, })); // Bad naming const useStore = create((set) => ({ data: [], loading: false, fetchData: async () => { set({ loading: true }); // ... fetch data set({ data: /* fetched data */, loading: false }); }, getData: (state) => state.data, getLoading: (state) => state.loading, })); """ ## 2. Zustand Specific Architecture These standards focus on using Zustand features in an architectural way that promotes maintainability, performance and scalability. ### 2.1 Standard: Immutability **Do This:** Treat state as immutable. Always update state with non-mutating methods. This means using the spread operator ("..."), "Object.assign()", array methods that return new arrays (".map()", ".filter()", ".slice()"), and immutable data structures if needed for performance-critical sections. This is especially critical given potential performance consequences of bypassing shallow equality checks. **Don't Do This:** Mutate state directly (e.g., "state.items.push(newItem)"). This will lead to unexpected behavior and prevent React from efficiently re-rendering components. **Why:** Immutability ensures predictable state updates, simplifies debugging, and enables efficient React re-renders by allowing shallow comparisons to detect changes. It's also crucial when using middleware like "immer" (described later). **Example:** """javascript import { create } from 'zustand'; const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), // Correct: Using spread operator removeItem: (itemId) => set((state) => ({ items: state.items.filter((item) => item.id !== itemId), // Correct: Using filter })), clearCart: () => set({ items: [] }), // Correct: Setting to a new empty array })); export default useCartStore; // Anti-pattern (MUTATING STATE): // addItem: (item) => set((state) => { state.items.push(item); return { items: state.items }; }), """ ### 2.2 Standard: Selectors for Performance Optimization **Do This:** Use selectors to derive specific data from the store. Selectors help components subscribe only to the parts of the state they need, preventing unnecessary re-renders. Use "React.memo" or similar techniques in conjunction with selectors for optimal performance of subscribed components. Leverage the referential equality of selector results to signal component updates. **Don't Do This:** Accessing the entire store in a component and then manually filtering or transforming the data. This forces the component to re-render whenever *any* part of the store changes. Also, avoid creating selectors inside the component, as they'll be re-created on every render, breaking memoization. **Why:** Selectors improve performance by minimizing unnecessary re-renders. **Example:** """javascript import { create } from 'zustand'; const useProductsStore = create((set) => ({ products: [ { id: 1, name: 'Product A', price: 20 }, { id: 2, name: 'Product B', price: 30 }, { id: 3, name: 'Product C', price: 40 }, ], // Selector: Returns only the product names selectProductNames: (state) => state.products.map((product) => product.name), // Selector: Returns a product by ID selectProductById: (state, productId) => state.products.find((product) => product.id === productId), })); export default useProductsStore; // Component using selectors import useProductsStore from './productStore'; import React from 'react'; const ProductList = React.memo(() => { const productNames = useProductsStore((state) => state.selectProductNames); console.log('ProductList re-rendered'); // Only re-renders when product names change return ( <ul> {productNames.map((name) => ( <li key={name}>{name}</li> ))} </ul> ); }); export default ProductList; const ProductDetails = ({ productId }) => { const product = useProductsStore( React.useCallback((state) => state.selectProductById(state, productId), [productId]) ); if (!product) { return <p>Product not found.</p>; } return ( <div> <h3>{product.name}</h3> <p>Price: ${product.price}</p> </div> ); }; """ The "React.memo" in "ProductList" combined with selector ensures the component only rerenders when the array of "productNames" changes (shallow equality comparison). The "ProductDetails" example leverages "useCallback" to memorize and avoid recreating the selector on every render. This avoids unnecessary re-renders and maintains consistent behaviour. ### 2.3 Standard: Middleware Usage and Composition **Do This:** Utilize Zustand's middleware capabilities for cross-cutting concerns like persistence, logging, and immer-based state mutations. Compose middleware carefully, considering their order of execution. Consider splitting complex middleware logic into smaller, reusable functions. **Don't Do This:** Overusing global middleware for feature-specific logic. Ignoring the execution order of middleware, as this can lead to unexpected behavior. **Why:** Middleware promotes separation of concerns, making stores cleaner and easier to test. **Examples:** """javascript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; // Logger middleware (example) const logger = (config) => (set, get, api) => config( (args) => { console.log(' applying', args); set(args); console.log(' new state', get()); }, get, api ); const useStore = create( logger( persist( immer((set) => ({ count: 0, increment: () => set((state) => { state.count += 1 }), // Immer-based mutation decrement: () => set((state) => { state.count -= 1 }), // Immer-based mutation })), { name: 'my-app-storage', storage: createJSONStorage(() => localStorage), } ) ) ); export default useStore; """ **Explanation:** * **"immer":** Allows you to write direct mutations to the state within the "set" function, while Immer handles the immutability under the hood, greatly simplifying complex updates. * **"persist":** Persists the store's state to a storage medium (in this case, "localStorage"). Make sure to use a "createJSONStorage" adapter for handling serialization. Configuration options allow specifying storage name, versioning, and custom (de)serialization. * **"logger":** Logs the state and modifications to the console. This aids in debugging. * **Order of Middleware:** "logger" wraps "persist", which wraps "immer". This means logging will include persisted data. ### 2.4 Standard: Asynchronous Actions and Side Effects **Do This:** Handle asynchronous operations and side effects within store actions. Use "async/await" syntax for cleaner asynchronous code. Consider using a separate utility function (outside the store) to manage the complexities of API requests. **Don't Do This:** Performing side effects directly within components or outside store actions. Directly mutating the store state within asynchronous callbacks. **Why:** Centralizing side effects within actions improves testability, maintainability, and predictability. **Example:** """javascript import { create } from 'zustand'; // Utility function for API requests (outside the store) const fetchProductsFromApi = async () => { const response = await fetch('/api/products'); if (!response.ok) { throw new Error('Failed to fetch products'); } return await response.json(); }; const useProductsStore = create((set) => ({ products: [], isLoading: false, error: null, fetchProducts: async () => { try { set({ isLoading: true, error: null }); const products = await fetchProductsFromApi(); // Use utility function set({ products: products, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useProductsStore; """ ### 2.5 Standard: Deriving State with "get" **Do This**: Inside your store, when a state update depends on the *previous* state *and* other values already in the state, use the "get" function provided by Zustand. This allows you to access the current state within the "set" callback. **Don't Do This**: Trying to access directly the state within the "set" callback without using the "get" function. This may lead to stale values and incorrect state updates. **Why**: "get" ensures you are working with the most up-to-date state within your store. This is particularly important and prevents race-conditions with async operations. **Example**: """javascript import { create } from 'zustand'; const useCounterStore = create((set, get) => ({ count: 0, step: 1, // Increment step increment: () => set(state => ({ count: state.count + get().step })), // Increment using the step setStep: (step) => set({ step: step }), // Updates the step })); export default useCounterStore; """ ### 2.6 Standard: Context Usage (Minimal) **Do This:** Prefer direct store usage with the "useStore" hook within components. Only use React Context if absolutely necessary, typically for providing initial state or global configurations to Zustand stores—and preferably using the "context" option in "create" hook. **Don't Do This:** Overuse React Context for passing frequently updated state, as this can negate the benefits of Zustand's selective re-rendering. **Why:** Zustand is designed to be used independently of React Context for most state management needs. Over-reliance on Context can hurt performance. **Example (minimal Context Usage):** """javascript // Store with initial value coming from a Context import { createContext, useContext } from 'react'; import { create } from 'zustand'; const ConfigContext = createContext({ apiEndpoint: '/default/api' }); const useProductsStore = create((set, get, api) => ({ ... }), {context: ConfigContext}); export default useProductsStore; const MyComponent = () => { const products = useProductsStore(state => state.products); return ( {/* ... */} ) } """ ### 2.7 Standard: Data Fetching Best Practices **Do This:** Separate data fetching logic from your Zustand store. Use custom hooks to encapsulate data fetching and update the store accordingly. **Don't Do This:** Directly performing data fetching inside components. This can lead to tight coupling and make testing difficult. **Why:** This improves code organization, testability, and reusability. **Example:** """javascript // Custom hook for fetching data import { useState, useEffect } from 'react'; import useProductsStore from './productStore'; const useFetchProducts = () => { const { fetchProducts } = useProductsStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { setIsLoading(true); setError(null); try { await fetchProducts(); } catch (err) { setError(err); } finally { setIsLoading(false); } }; fetchData(); }, [fetchProducts]); return { isLoading, error }; }; export default useFetchProducts; // Component using the custom hook import useFetchProducts from './useFetchProducts'; import useProductsStore from './productStore'; const ProductList = () => { const { isLoading, error } = useFetchProducts(); const products = useProductsStore((state) => state.products); if (isLoading) { return <p>Loading products...</p>; } if (error) { return <p>Error: {error.message}</p>; } return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ); }; export default ProductList; """ These standards, when applied consistently, will result in a more robust, maintainable, and performant Zustand-based application. Remember to prioritize clarity and modularity in your architecture, and choose the right tool for the job (selector or whole state).
# Component Design Standards for Zustand This document outlines the coding standards for component design when using Zustand for state management in React applications. These standards promote reusability, maintainability, and performance, and are tailored for the latest Zustand features and best practices. ## 1. General Principles ### 1.1. Single Responsibility Principle (SRP) * **Do This:** Design components so each has only one reason to change. For example, a component rendering user data shouldn't also handle authentication. Separate concerns into distinct components (some of which internally use zustand stores). * **Don't Do This:** Create monolithic components handling multiple unrelated functionalities. This makes debugging and maintenance difficult. * **Why:** SRP leads to more modular, testable, and understandable code. Changes in one area are less likely to impact unrelated parts of the application. * **Example:** """jsx // Good: Separate components for user data and profile update const UserData = () => { const userData = useStore(state => state.user); return ( <div> <h1>{userData.name}</h1> <p>{userData.email}</p> </div> ); }; const UpdateProfile = () => { const updateUser = useStore(state => state.updateUser); const [name, setName] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); updateUser({name}); } return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <button type="submit">Update Name</button> </form> ); }; """ """jsx // Bad: A single component doing too much const UserProfile = () => { const userData = useStore(state => state.user); const updateUser = useStore(state => state.updateUser); const [name, setName] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); //Also handles OTHER unrelated operations here. BAD. updateUser({name}); } return ( <div> <h1>{userData.name}</h1> <p>{userData.email}</p> <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <button type="submit">Update Name</button> </form> </div> ); }; """ ### 1.2. Separation of Concerns (SoC) * **Do This:** Isolate distinct parts of your application (UI, state management, data fetching) into separate modules. Use custom hooks to abstract Zustand logic from components, promoting better SoC - especially for reading data. For mutations write discrete reusable setters in the zustand store. * **Don't Do This:** Mix UI rendering logic directly with state management or data fetching within a single component. * **Why:** SoC improves maintainability, testability, and reusability. It also makes it easier to understand and modify specific parts of the application. * **Example:** """jsx // Good: Custom hook for fetching and exposing user data import create from 'zustand'; // Ensure you import create from zustand const useUserStore = create((set) => ({ user: null, fetchUser: async (id) => { const response = await fetch("/api/users/${id}"); const data = await response.json(); set({ user: data }); }, updateUser: (newUserData) => set(state => ({ ...state, user: { ...state.user, ...newUserData } })), })); // User component consuming the hook const UserProfile = () => { const user = useUserStore(state => state.user); const fetchUser = useUserStore(state => state.fetchUser); React.useEffect(() => { fetchUser(123); // Example user ID }, [fetchUser]); if (!user) { return <p>Loading user data...</p>; } return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }; """ """jsx // Bad: Mixing data fetching and UI rendering const UserProfileBad = () => { const [user, setUser] = React.useState(null); React.useEffect(() => { const fetchUser = async (id) => { const response = await fetch("/api/users/${id}"); const data = await response.json(); setUser(data); }; fetchUser(123); }, []); if (!user) { return <p>Loading user data...</p>; } return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }; """ ### 1.3. Component Composition * **Do This:** Favor composition over inheritance. Create smaller, focused components and combine them to build more complex UIs. * **Don't Do This:** Create deep inheritance hierarchies, leading to inflexible and hard-to-understand code. * **Why:** Composition provides more flexibility and reusability. It allows you to easily combine and rearrange components to create diverse UIs. * **Example:** """jsx // Good: Composing smaller components const Button = ({ children, onClick }) => ( <button onClick={onClick}>{children}</button> ); const Input = ({ type, value, onChange }) => ( <input type={type} value={value} onChange={onChange} /> ); const Form = () => { const [email, setEmail] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); alert("Email submitted: ${email}"); }; return ( <form onSubmit={handleSubmit}> <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <Button type="submit">Submit</Button> </form> ); }; """ ## 2. Zustand-Specific Component Design ### 2.1. Selective Subscriptions with "useStore" * **Do This:** Use the selector function in "useStore" to subscribe only to the parts of the store that a component actually needs. This minimizes re-renders, boosting performance. Leverage "useShallow" and "equalityFn" options when appropriate. * **Don't Do This:** Subscribe to the entire store if the component only needs a small piece of data. This leads to unnecessary re-renders. * **Why:** Selective subscriptions prevent components from re-rendering when unrelated parts of the store change. This significantly improves performance, especially in complex applications. * **Example:** """jsx // Good: Selecting only the "todos" array import create from 'zustand'; import { shallow } from 'zustand/shallow'; const useTodoStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) })), filter: 'all', setFilter: (filter) => set({ filter }), })); const TodoList = () => { //Selects only the todos and toggleTodo from the zustand store. const [todos, toggleTodo] = useTodoStore(state => [state.todos, state.toggleTodo], shallow); return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} </li> ))} </ul> ); }; """ """jsx // Bad: Subscribing to the entire store (BAD PRACTICE) const TodoListBad = () => { const { todos, toggleTodo } = useTodoStore(); // Gets EVERYTHING return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} </li> ))} </ul> ); }; """ ### 2.2. Derived State with Selectors * **Do This:** Use selectors within "useStore" to derive new state values from the store's state. Avoid performing calculations directly within the component if that derived value is needed in multiple components or could be memoized for performance gains. * **Don't Do This:** Recompute derived state within components on every render, especially if the calculations are expensive. * **Why:** Selectors allow you to efficiently compute derived state and only trigger re-renders when the relevant parts of the store change. * **Example:** """jsx import create from 'zustand'; const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) })), })); const CartTotal = () => { // Selector to calculate the total price const total = useCartStore(state => state.items.reduce((sum, item) => sum + item.price, 0) ); return ( <p>Total: ${total}</p> ); }; """ ### 2.3. Using "useStore.getState" Sparingly * **Do This:** Primarily use "useStore(selector)" for accessing state within components. Reserve "useStore.getState()" for cases where you need to access the store's value outside of the component lifecycle (ex: event handlers, testing, or directly interacting with the store). * **Don't Do This:** Rely heavily on "useStore.getState()" inside React component render functions; this bypasses subscriptions and can lead to stale data and performance issues. * **Why:** "useStore(selector)" drives efficient component re-renders and reactivity based on zustand store changes. "useStore.getState()" is a snapshot, and does not cause functional components using reactiveness to re-render.. * **Example:** """jsx import create from 'zustand'; const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); const CounterComponent = () => { const count = useCounterStore((state) => state.count); // Use selector for reactivity const increment = useCounterStore(state => state.increment); //Example of getState usage. const logCount = () => { //This is fine because it's not in the render function directly. const currentCount = useCounterStore.getState().count; console.log('Current count:', currentCount); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={logCount}>Log count</button> </div> ); }; """ ### 2.4 Local vs global state * **Do This:** Use local state for presentational and UI concerns. Use Zustand to reflect your data model. * **Don't Do This:** Put EVERYTHING into Zustand. Components should continue to manage transient UI-related state (form input values, active tab state, etc) without involving Zustand, unless that state needs to be shared or persisted. * **Why:** Over-using Zustand can lead to unnecessary complexity and performance overhead. Local state can be more efficient for component-specific concerns with limited scope. * **Example:** """jsx // Good: Local state for input value, Zustand for user data import create from 'zustand'; const useUserStore = create((set) => ({ user: { name: '', email: '' }, updateUser: (userData) => set({ user: userData }), })); const UserForm = () => { const [name, setName] = React.useState(''); // Local state for input const [email, setEmail] = React.useState(''); // Local state const updateUser = useUserStore(state => state.updateUser); const handleSubmit = (e) => { e.preventDefault(); updateUser({ name, email }); // Update Zustand store }; return ( <form onSubmit={handleSubmit}> <label>Name: <input type="text" value={name} onChange={(e) => setName(e.target.value)} /></label> <label>Email: <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></label> <button type="submit">Update Profile</button> </form> ); }; """ ### 2.5 Managing Asynchronous Actions * **Do This:** Define async actions (like data fetching) within your Zustand store. Use "try...catch" blocks to handle potential errors and update the store's state accordingly (e.g., setting an "error" flag). If possible, isolate your data access logic using custom hooks to help with SoC and reusability. * **Don't Do This:** Perform data fetching directly within your components without using any error handling and proper state management. Avoid mutating store values directly within the components. * **Why:** Keep your code clean, maintainable, and testable. Centralized async actions are easier to manage and provide a single source of truth. * **Example:** """jsx import create from 'zustand'; const useProductsStore = create((set) => ({ products: [], loading: false, error: null, fetchProducts: async () => { set({ loading: true, error: null }); try { const response = await fetch('/api/products'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); set({ products: data, loading: false }); } catch (e) { set({ error: e.message, loading: false }); } }, })); const ProductsList = () => { const {products, loading, error, fetchProducts} = useProductsStore(); useEffect(() => { fetchProducts(); }, [fetchProducts]); if (loading) return <p>Loading products...</p>; if (error) return <p>Error: {error}</p>; return ( <ul> {products.map(product => ( <li key={product.id}>{product.name} - ${product.price}</li> ))} </ul> ); }; """ ## 3. Advanced Patterns and Considerations ### 3.1. Zustand Middleware * **Do This:** Leverage Zustand's middleware features (e.g., "persist", "devtools", "immer") to add cross-cutting concerns to your state management without cluttering component logic. * **Don't Do This:** Implement persistence, debugging, or immutable updates manually in components. * **Why:** Middleware provides a clean and composable way to enhance Zustand stores with features like persistence, debugging, and immutable updates. * **Example:** """jsx import create from 'zustand'; import { persist } from 'zustand/middleware'; import { devtools } from 'zustand/middleware'; import produce from 'immer'; const useStore = create(devtools(persist( (set) => ({ todos: [], addTodo: (text) => set(produce((state) => { state.todos.push({ id: Date.now(), text, completed: false }); })), }), { name: 'todo-store', // unique name getStorage: () => localStorage, } ))); """ ### 3.2. Testing Components with Zustand * **Do This:** Mock the Zustand store in your component tests to isolate the component and make tests predictable. Consider using a testing library (e.g., Jest, React Testing Library) and mock the Zustand store using "jest.mock". Use "act" when updating the Zustand store within tests. * **Don't Do This:** Directly manipulate the Zustand store in tests without using mocking, leading to unpredictable test results and potential interference between tests. * **Why:** Mocking allows you to control the store's state and actions during testing, ensuring consistent and reliable test results and preventing interference between tests. * **Example:** """jsx // __mocks__/zustand.js const actualZustand = jest.requireActual('zustand'); let storeValues = {}; const mockSet = (fn) => { storeValues = typeof fn === "function" ? fn(storeValues) : fn; }; const useStore = () => ({ getState: () => storeValues, setState: mockSet, subscribe: () => {}, destroy: () => {} }); useStore.setState = mockSet; useStore.getState = () => storeValues; useStore.subscribe = () => {}; useStore.destroy = () => {}; module.exports = actualZustand; module.exports.create = () => useStore module.exports.useStore = useStore // Component.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import TodoList from './TodoList'; import * as zustand from 'zustand'; jest.mock('zustand'); describe('TodoList Component', () => { it('allows adding a todo to the list', () => { const mockStore = { todos: [], addTodo: jest.fn() }; const { useStore } = zustand; useStore.mockReturnValue(mockStore); render(<TodoList />); const inputElement = screen.getByPlaceholderText('Add todo'); const addButtonElement = screen.getByText('Add'); act(() => { fireEvent.change(inputElement, { target: { value: 'Buy milk' } }); fireEvent.click(addButtonElement); }); expect(mockStore.addTodo).toHaveBeenCalledWith('Buy milk'); }); }); """ ### 3.3. Performance Optimization Strategies * **Do This:** Use "React.memo" and "useCallback" in conjunction with Zustand selectors to prevent unnecessary re-renders of components. Make sure your custom hook returns a stable reference of the store or selector. * **Don't Do This:** Rely solely on Zustand to optimize performance, without considering React's own optimization techniques. * **Why:** "React.memo" memoizes components based on their props. Using useCallback on the state management actions passed as props can drastically eliminate rerenders when props are exactly the same. ### 3.4 Type Safety with TypeScript * **Do This:** Use TypeScript to define the shape of your Zustand store's state and actions. This provides compile-time type checking and helps prevent runtime errors. * **Example:** """typescript import create from 'zustand'; interface UserState { id: number; name: string; email: string; } interface UserActions { updateName: (newName: string) => void; updateEmail: (newEmail: string) => void; } type UserStore = UserState & UserActions; const useUserStore = create<UserStore>((set) => ({ id: 1, name: 'John Doe', email: 'john.doe@example.com', updateName: (newName) => set({ name: newName }), updateEmail: (newEmail) => set({ email: newEmail }), })); """ These coding standards for Zustand component design will help you write maintainable, performant, and testable React applications. By following these guidelines, your team can contribute to a codebase that is easy to understand, modify, and extend.
# State Management Standards for Zustand This document provides comprehensive coding standards for state management using Zustand. These standards are designed to ensure maintainable, performant, and secure applications. It is tailored for developers of all experience levels, providing guidance for code clarity and consistency. ## 1. Core Principles of Zustand State Management ### 1.1 Simplicity and Minimalism **Do This:** * Embrace Zustand's simplicity. Keep state definitions concise and business-logic-focused. Avoid over-complicating state with derived data unless absolutely necessary. **Don't Do This:** * Introduce unnecessary abstractions or layers of complexity. Zustands strength lies in direct access and mutations. Don't try to mimic Redux-style architectures. **Why:** Simplicity reduces cognitive load, enables faster development, and makes debugging easier. It also aligns with Zustands core philosophy. **Example:** """javascript import { create } from 'zustand' const useBearStore = create((set) => ({ bears: 0, increaseBears: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) export default useBearStore; """ ### 1.2 Explicit Data Flow **Do This:** * Define clear data flow paths. Actions within the store should be the primary way to update the state. This promotes maintainability and traceability. **Don't Do This:** * Mutate state directly outside of the store. This breaks the unidirectional data flow, which makes debugging and reasoning about your application state difficult. **Why:** Explicit data flow improves predictability and makes it easier to track state changes. **Example:** """javascript import { create } from 'zustand' const useProductsStore = create((set) => ({ products: [], fetchProducts: async () => { const response = await fetch('/api/products'); // Simulating product API const data = await response.json(); set({ products: data }); }, addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductsStore; """ ### 1.3 Reactivity and Selectors **Do This:** * Use selectors to extract specific parts of the state that components need. Subscribing components only to the necessary parts of the state enhances performance. * Memoize selectors to avoid unnecessary re-renders. When using selectors frequently, especially with large state objects, memoization can substantially reduce render load. **Don't Do This:** * Subscribe components to the entire store state when only a small part of it is needed. * Compute values directly within components that are derived from the state. This tightly couples logic to components and makes it hard to optimize re-renders. **Why:** Selectors and memoization improve performance by reducing the amount of re-rendering in your React components. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) })), totalItems: () => { // Derived Value, could be enhanced return useCartStore.getState().items.length; }, totalPrice: () => { // Derived Value, could be enhanced return useCartStore.getState().items.reduce((acc, item) => acc + item.price, 0) } })) // Component Using Selectors, SHALLOW comparison for re-renders! function CartSummary() { const [totalItems, totalPrice] = useCartStore( (state) => [state.totalItems, state.totalPrice], shallow ) return ( <div> Total Items: {totalItems()} Total Price: ${totalPrice()} </div> ) } export default useCartStore; """ ### 1.4 Asynchronous Actions **Do This:** * Handle asynchronous logic (e.g., API fetching) directly inside store actions. Use "async/await" or "Promises" for clarity. **Don't Do This:** * Perform asynchronous actions outside of the store and then update the state. This can lead to race conditions or inconsistent state updates. **Why:** Centralizing asynchronous operations within the store provides better control and error handling. **Example:** """javascript import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, isLoading: false, error: null, fetchUser: async (userId) => { set({ isLoading: true, error: null }); // Set loading state try { const response = await fetch("/api/users/${userId}"); const data = await response.json(); set({ user: data, isLoading: false }); // Update user data and reset loading } catch (error) { set({ error: error.message, isLoading: false }); // Set error state } }, })); export default useUserStore; """ ## 2. Zustand Store Structure and Organization ### 2.1 Modular Stores **Do This:** * Divide your application state into smaller, manageable stores based on logical domains (e.g., user, product, cart). Favor Composition over Inheritance. **Don't Do This:** * Create a single monolithic store for the entire application if your app grows complex. This will lead to performance bottlenecks and make the code hard to follow. **Why:** Modular stores improve code organization, reduce the scope of changes, and make it easier to test and reuse state logic. **Example:** """javascript // userStore.js import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user }), })); export default useUserStore; // productStore.js import { create } from 'zustand' const useProductStore = create((set) => ({ products: [], addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductStore; // cartStore.js import { create } from 'zustand' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })); export default useCartStore; """ ### 2.2 Group Related State **Do This:** * Logically group pieces of the store that are related. Avoid scattering related state properties across the store definition. **Don't Do This:** * Separating the "isLoading" loading flag, and error states from async actions that they're associated with. **Why:** Improve code readability and developer experience. **Example:** """javascript import { create } from 'zustand' const useAuthStore = create((set) => ({ authState: { // Grouping properties isAuthenticated: false, token: null, isLoading: false, error: null }, login: async (username, password) => { set((state) => ({ authState: { ...state.authState, isLoading: true, error: null } })); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const data = await response.json(); set((state) => ({ authState: { isAuthenticated: true, token: data.token, isLoading: false, error: null } })); } catch (error) { set((state) => ({ authState: { ...state.authState, isLoading: false, error: error.message } })); } }, logout: () => { set((state) => ({ authState: { isAuthenticated: false, token: null, isLoading: false, error: null } })); }, })); export default useAuthStore; """ ### 2.3 Using "persist" Middleware **Do This:** * Use "zustand/middleware"'s "persist" middleware when you need to persist state across browser sessions. This is especially useful for user authentication or cart data. Configuring the storage adapter when necessary. **Don't Do This:** * Persist sensitive information directly into local storage without proper encryption. * Persisting state unnecessarily without consideration for the limited local storage budget. **Why:** Persistence enhances user experience by preserving state across sessions but necessitates cautious handling of sensitive data. **Example:** """javascript import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' const useSettingsStore = create( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'settings-storage', // unique name storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used } ) ); export default useSettingsStore; """ ## 3. Zustand Action Design ### 3.1 Atomic Updates **Do This:** * Design actions to perform atomic updates to the state. Each action should represent a single, logical change to the state. **Don't Do This:** * Bunch unrelated state updates in a single action, making maintenance difficult. * Create actions that trigger cascading updates across multiple stores. This tightens coupling. **Why:** Atomic updates lead to simpler and more predictable state transitions, which are easier to test and debug. **Example:** """javascript import { create } from 'zustand' const useTaskStore = create((set) => ({ tasks: [], addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })), removeTask: (taskId) => set((state) => ({ tasks: state.tasks.filter((task) => task.id !== taskId) })), updateTaskStatus: (taskId, completed) => // Focused Update set((state) => ({ tasks: state.tasks.map((task) => task.id === taskId ? { ...task, completed } : task ), })), })); export default useTaskStore; """ ### 3.2 Naming Conventions **Do This:** * Use clear and consistent naming conventions for actions. Action names should describe what they do. A good naming system is "verbNoun": e.g. "addProduct", "removeUser" etc. **Don't Do This:** * Use vague or ambiguous action names that don't clearly indicate their purpose. **Why:** Consistent naming ensures code readability and makes it easier to understand the purpose of each action. **Example:** """javascript import { create } from 'zustand' const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); export default useCounterStore; """ ### 3.3 Decoupled Logic **Do This:** * Keep store logic focused on state updates. Delegate complex business logic to separate utility functions or services. **Don't Do This:** * Embed complex computations or algorithms directly within the state update functions. **Why:** Decoupled logic makes stores easier to understand, test, and maintain. **Example:** """javascript // utils/taxCalculator.js export const calculateTax = (price, taxRate) => { return price * taxRate; }; // store.js import { create } from 'zustand' import { calculateTax } from './utils/taxCalculator'; const useProductStore = create((set) => ({ price: 100, taxRate: 0.10, setTaxRate: (rate) => set({ taxRate: rate }), calculateTaxedPrice: () => { // Moved the calculateTax logic to external utility. Very easy to test! return useProductStore.getState().price + calculateTax(useProductStore.getState().price, useProductStore.getState().taxRate); }, })); export default useProductStore; """ ## 4. Optimization and Performance ### 4.1 Shallow Equality **Do This:** * When selecting state, use "shallow" from "zustand/shallow" for shallow equality checks to prevent unnecessary re-renders. * Structure your state to encourage shallow comparisons. Avoid deeply nested objects when possible. **Don't Do This:** * Skip shallow equality checks when performance is important. * Modify nested objects within the state immutably; otherwise, shallow comparisons won't work as expected. **Why:** Shallow equality ensures that components only re-render when the relevant state actually changes, reducing unnecessary updates. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useProfileStore = create((set) => ({ profile: { name: 'John Doe', age: 30 }, updateName: (name) => set((state) => ({ profile: { ...state.profile, name } })), updateAge: (age) => set((state) => ({ profile: { ...state.profile, age } })), })); function ProfileComponent() { const { name, age } = useProfileStore( (state) => ({ name: state.profile.name, age: state.profile.age }), shallow ); return ( <div> Name: {name}, Age: {age} </div> ); } export default useProfileStore; """ ### 4.2 Minimize State Size **Do This:** * Only store essential data in Zustand store. Avoid duplicating data or storing derived data if it can be easily computed. **Don't Do This:** * Inflate stores with large or redundant data, increasing memory usage and potentially affecting performance. **Why:** Minimize state size improves performance by reducing the overhead of state updates and re-renders. ### 4.3 Immutability **Do This:** * Ensure that all operations in the store are done immutably to trigger re-renders. * Use "..." spread operator or lodash/immer to ensure immutability **Don't Do This:** * Directly mutate the state. **Why:** Immutability ensure that new references are created and React knows to re-render the component to reflect the changes in the store. **Example:** """javascript import { create } from 'zustand' const useTodosStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), })), })); export default useTodosStore; """ ## 5. Testing ### 5.1 Unit Testing **Do This:** * Write unit tests for your Zustand stores to ensure that actions update the state correctly. * Test actions with different inputs and check for expected state changes. **Don't Do This:** * Skip testing Zustand stores, assuming they are simple and error-free. **Why:** Unit tests ensure that your state management logic is correct. **Example:** """javascript import { create } from 'zustand' import { act } from 'react-dom/test-utils'; import { renderHook } from '@testing-library/react'; const useTestStore = create((set) => ({ value: 0, increment: () => set((state) => ({ value: state.value + 1 })), setValue: (newValue) => set({ value: newValue }), })); describe('useTestStore', () => { beforeEach(() => { // Reset the store before each test useTestStore.setState({value: 0}, true); }); it('should initialize with a value of 0', () => { const { result } = renderHook(() => useTestStore()); expect(result.current.value).toBe(0); }); it('should increment the value by 1', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.increment(); }); expect(useTestStore.getState().value).toBe(1); }); it('should set the value to a new value', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.setValue(10); }); expect(useTestStore.getState().value).toBe(10); }); }); export default useTestStore; """ ### 5.2 Integration Testing **Do This:** * Test how Zustand stores interact with your React components and other parts of your application. **Don't Do This:** * Ignore integration tests, as they ensure that the entire system works correctly. **Why:** Integration tests verify that your state management solution integrates seamlessly with the rest of your application. ### 5.3 Mocking Dependencies **Do This:** * Mock external dependencies like APIs in your tests to ensure that tests are fast and reliable. **Don't Do This:** * Rely on live APIs in your tests, which can make the tests slow and flaky. **Why:** Mocking dependencies isolates your components and stores, allowing you to test them in a controlled environment. ## 6. Security Considerations ### 6.1 Data Encryption **Do This:** * If you are persisting sensitive data using "zustand/middleware" persisted state, encrypt the data before storing it in local storage. **Don't Do This:** * Store sensitive data in plain text. **Why:** Encryption protects sensitive data from unauthorized access, ensuring a higher level of security for your application. ### 6.2 Input Validation **Do This:** * Validate all inputs to Zustand store actions to prevent invalid data from being stored. **Don't Do This:** * Trust user input without validation. **Why:** Input validation prevents security vulnerabilities like cross-site scripting (XSS) and SQL injection. ### 6.3 Authentication and Authorization **Do This:** * Implement proper authentication and authorization mechanisms to protect sensitive state and actions. **Don't Do This:** * Expose sensitive actions or state to unauthorized users. **Why:** Authentication and authorization ensure that only authorized users can access and modify sensitive data, protecting your application from malicious attacks. ## 7. Conclusion By adhering to these Zustand coding standards, you can create maintainable, performant, and secure state management solutions. Zustand provides a simple and powerful way to manage state in React applications, and these guidelines will help you use it effectively. Remember to prioritize simplicity, clarity, and testability in your code.
# Performance Optimization Standards for Zustand This document outlines best practices for optimizing the performance of Zustand stores in React applications. It aims to provide clear guidelines for developers to write efficient, responsive, and resource-friendly code when using Zustand. ## 1. Selective State Updates ### Standard 1: Avoid Unnecessary Rerenders with Shallow Equality **Do This:** Use "shallow" equality check within "useStore" selector functions to prevent rerenders when the selected state doesn't actually change. **Don't Do This:** Directly destructure state inside "useStore" without a selector or without shallow comparison, as this will cause rerenders on every store update, even if relevant data hasn't changed. **Why This Matters:** React components rerender when their props change. Zustand's "useStore" hook triggers a rerender whenever the store updates unless you use a selector. Without shallow equality, even if the underlying data within the store remains the same, the component will rerender, leading to performance bottlenecks, especially in complex UIs. **Code Example:** """javascript import { create } from 'zustand'; import { shallow } from 'zustand/shallow'; const useStore = create((set) => ({ user: { id: 1, name: 'John Doe', age: 30 }, updateUserAge: (age) => set(state => ({ user: { ...state.user, age } })) })); function UserProfile() { // Correct: Using a selector with shallow equality, avoids rerenders // if only other properties of "user" change const { name, age } = useStore((state) => ({ name: state.user.name, age: state.user.age }), shallow); // Incorrect: Direct destructuring causes rerenders on every store update even if name and age didn't change. // const { user, updateUserAge } = useStore(); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); } export default UserProfile; """ ### Standard 2: Minimize Selector Granularity **Do This:** Select only the specific data a component needs from the Zustand store. Create multiple, focused selectors within a component if necessary. **Don't Do This:** Select large portions of the store and then filter or transform the data within the component. This triggers rerenders even if the component only uses a small subset of the selected data and results in more processing work per render. **Why This Matters:** The goal is to minimize the amount of data that triggers a rerender. Selecting only essential data ensures rerenders are triggered only when absolutely necessary. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ items: [ { id: 1, name: 'Apple', price: 1.0 }, { id: 2, name: 'Banana', price: 0.5 }, { id: 3, name: 'Orange', price: 0.75 }, ], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })); function ItemList() { // Correct: Selects only item names and prices const itemNamesAndPrices = useStore(state => state.items.map(item => ({ name: item.name, price: item.price }))); // Incorrect: Selecting the entire "items" array. If we only need the names and prices, it's inefficient. // const items = useStore(state => state.items); return ( <ul> {itemNamesAndPrices.map(item => ( <li key={item.name}> {item.name} - ${item.price} </li> ))} </ul> ); } export default ItemList; """ ### Standard 3: Memoize Selectors for Complex Transformations **Do This:** Use memoization techniques (e.g., "useMemo" in React, or libraries like "reselect") for selectors that perform expensive calculations or transformations. **Don't Do This:** Perform the same complex calculations within selectors on every render. This adds overhead and slows down the application, especially with large datasets. **Why This Matters:** Memoization caches the results of expensive operations, returning the cached result if the input dependencies haven't changed. This avoids redundant calculations and significantly enhances performance. **Code Example:** """javascript import { create } from 'zustand'; import { useMemo } from 'react'; const useStore = create((set) => ({ products: [ { id: 1, name: 'Laptop', price: 1200, category: 'Electronics' }, { id: 2, name: 'Keyboard', price: 100, category: 'Electronics' }, { id: 3, name: 'T-Shirt', price: 25, category: 'Clothing' }, ], })); function ProductList({ category }) { const products = useStore(state => state.products); // Memoize the filtered products based on the selected category const filteredProducts = useMemo(() => { console.log('Filtering products...'); // This should only log when the 'category' prop changes. return products.filter(product => product.category === category); }, [products, category]); // Depend on 'products' *and* 'category'! return ( <ul> {filteredProducts.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> ); } export default ProductList; """ ## 2. Optimizing Store Updates ### Standard 4: Batch Multiple State Updates **Do This:** Use the functional form of "set" to batch multiple related state updates within a single call. **Don't Do This:** Trigger multiple independent calls to "set" for related state updates. Each "set" triggers rerenders, so batching reduces the total number of rerenders improving performance. **Why This Matters:** React can batch updates, reducing the overhead of multiple rerenders. By combining related updates into single "set" operations, you can improve application responsiveness. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, name: 'Initial Name', incrementAndRename: () => set(state => ({ count: state.count + 1, name: "Name ${state.count + 1}" })), // Batched into a single update //Don't Do this: increment: () => set(state => ({count: state.count + 1})), rename: (newName) => set({ name: newName}) })); function Counter() { const { count, name, incrementAndRename } = useStore(); return ( <div> <p>Count: {count}</p> <p>Name: {name}</p> <button onClick={incrementAndRename}>Increment and Rename</button> </div> ); } export default Counter; """ ### Standard 5: Debounce or Throttle Frequent Updates **Do This:** Use debouncing or throttling techniques when handling frequent and potentially redundant data updates (e.g., handling input changes, resizing). Libraries like "lodash" provide utilities for this. **Don't Do This:** Directly update the store on every single event trigger if the intermediate values are not important. This can lead to excessive rerenders and performance degradation. **Why This Matters:** Debouncing and throttling limit the rate at which a function is executed. Debouncing ensures a function is only called after a certain amount of time has passed since the last call. Throttling ensures a function is called at most once within a specified period. This is crucial for performance when dealing with high-frequency events. **Code Example:** """javascript import { create } from 'zustand'; import { debounce } from 'lodash'; const useStore = create((set) => ({ searchTerm: '', setSearchTerm: debounce((term) => { console.log("Updating search term to ${term}"); set({ searchTerm: term }); }, 300), // Debounce for 300ms })); function SearchInput() { const { searchTerm, setSearchTerm } = useStore(); const handleChange = (event) => { setSearchTerm(event.target.value); }; return ( <input type="text" value={searchTerm} onChange={handleChange} placeholder="Search..." /> ); } export default SearchInput; """ ### Standard 6: Leverage "useStore.setState"'s Immutability **Do This:** Within "useStore.setState", always return *new* objects and arrays rather than modifying existing ones. **Don't Do This:** Mutate existing arrays or objects directly within the "set" function. This can lead to unexpected behavior and difficulties in tracking state changes. **Why This Matters:** Zustand relies on immutability to determine when state changes occur. Mutating state directly can bypass this mechanism, causing components not to rerender correctly or lead to subtle bugs. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ items: [{ id: 1, name: 'Item 1' }], addItem: (newItem) => set((state) => ({ items: [...state.items, newItem], // Correct: Creates a new array with added item. })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) //Correct: Creates a new filtered array })) })); function ItemList() { const { items, addItem, removeItem } = useStore(); return ( <ul> {items.map(item => ( <li key={item.id}> {item.name} <button onClick={() => removeItem(item.id)}>Remove</button> </li> ))} </ul> ); } export default ItemList; """ ## 3. Code Splitting and Lazy Initialization ### Standard 7: Lazy Load Zustand Stores **Do This:** Use dynamic imports or React's "lazy" function to load Zustand stores only when they are needed. **Don't Do This:** Import all Zustand stores eagerly at the top level of your application. This increases the initial load time of the application. **Why This Matters:** Code splitting divides your application into smaller chunks, which can be loaded on demand. Lazy loading Zustand stores reduces the initial JavaScript bundle size, improving the initial load time and perceived performance. **Code Example:** """javascript import React, { lazy, Suspense } from 'react'; const LazyUserProfile = lazy(() => import('./UserProfile')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyUserProfile /> </Suspense> ); } export default App; """ Where "UserProfile.js" uses a Zustand store: """javascript // UserProfile.js import { create } from 'zustand'; const useStore = create((set) => ({ name: 'John Doe', age: 30, })); function UserProfile() { const { name, age } = useStore(); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); } export default UserProfile; """ ### Standard 8: Chunk Large Stores **Do This:** If a single Zustand store contains a large amount of data, consider breaking it down into smaller, more manageable stores. **Don't Do This:** Store a massive, monolithic state object in a single Zustand store, as this can lead to performance issues and make the application harder to maintain. **Why This Matters:** Dividing a large store into smaller stores isolates state updates and reduces the scope of rerenders. It improves organization and makes the application more scalable. ## 4. Avoiding Anti-Patterns ### Standard 9: Avoid Over-Storing Derived State **Do This:** Calculate derived state within components or with memoized selectors instead of storing redundant derived data in the Zustand store. **Don't Do This:** Store derived data directly in the store if it can be efficiently computed from existing state. This can lead to unnecessary updates and increased complexity. **Why This Matters:** Storing derived data can introduce inconsistencies and complex update logic. Calculating derived data on demand or with memoized selectors keeps the store simple and avoids redundant updates. **Code Example:** """javascript import { create } from 'zustand'; import { useMemo } from 'react'; const useStore = create((set) => ({ items: [ { id: 1, name: 'Apple', price: 1.0, quantity: 2 }, { id: 2, name: 'Banana', price: 0.5, quantity: 5 }, ], })); function ShoppingCart() { const items = useStore(state => state.items); // Correct: derived state calculated using useMemo. const totalCost = useMemo(() => { return items.reduce((acc, item) => acc + item.price * item.quantity, 0); }, [items]); return ( <div> <ul> {items.map(item => ( <li key={item.id}> {item.name} - ${item.price} x {item.quantity} </li> ))} </ul> <p>Total Cost: ${totalCost}</p> </div> ); } export default ShoppingCart; """ ### Standard 10: Detach Long-Running Operations **Do This:** For long-running operations or side effects, use asynchronous actions within the store and, where appropriate, separate the operation entirely (e.g. into a web worker). Avoid blocking the main thread. **Don't Do This:** Perform synchronous, long-running operations directly within the "set" function; this will block the UI and degrade the user experience. **Why This Matters:** Blocking the main thread causes the UI to freeze and become unresponsive during long calculations and operations. By using asynchronous actions and potentially web workers, you can keep the UI responsive. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ data: null, loading: false, fetchData: async () => { set({ loading: true }); try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); set({ data, loading: false }); } catch (error) { console.error('Error fetching data:', error); set({ loading: false, data: null }); // handle the error case correctly } }, })); function DataDisplay() { const { data, loading, fetchData } = useStore(); useEffect(() => { fetchData(); }, [fetchData]); if (loading) { return <div>Loading...</div>; } if (!data) { return <div>Error loading data.</div>; } return ( <div> <p>Data: {JSON.stringify(data)}</p> </div> ); } export default DataDisplay; """ ## 5. Specific Zustand Features ### Standard 11: Using the "devtools" Middleware (Development Only) **Do This:** Wrap your Zustand store creation with the "devtools" middleware during development to enable time-travel debugging and state inspection. Ensure this middleware is removed or disabled in production builds. **Don't Do This:** Leave the "devtools" middleware enabled in production, as it adds overhead and exposes internal state. **Why This Matters:** The "devtools" middleware enhances the development process by providing valuable insights. Disabling it in production ensures optimal performance and security. **Code Example:** """javascript import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; const useStore = create(devtools((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'MyStore' })); // Provide a unique name for the store function Counter() { const { count, increment } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } export default Counter; """ In production: """javascript import { create } from 'zustand'; // devtools middleware removed const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); function Counter() { const { count, increment } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } export default Counter; """ ### Standard 12: Integrate with Persist Middleware Sparingly **Do This:** Use the "persist" middleware judiciously, only for essential data that needs to be persisted across sessions. Consider encryption for sensitive data. **Don't Do This:** Persist large amounts of unimportant or frequently changing data, as this affects application performance and storage usage. **Why This Matters:** Persistent storage can impact application performance and create potential security vulnerabilities and privacy concerns. Use with care. **Code Example:** """javascript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; const useStore = create(persist((set) => ({ token: null, setToken: (token) => set({ token }), removeToken: () => set({ token: null }), }), { name: 'auth-storage', // unique name storage: createJSONStorage(() => localStorage), partialize: (state) => ({ token: state.token }), // Only persist the token } )); function AuthComponent() { const { token, setToken, removeToken } = useStore(); return ( // ... ); } export default AuthComponent; """ ## 6. Testing and Monitoring ### Standard 13: Monitor Store Performance **Do This:** Use performance monitoring tools (e.g., React Profiler, browser developer tools) to identify performance bottlenecks related to Zustand stores. Track render times and update frequencies. **Don't Do This:** Assume your Zustand stores are performant without proper monitoring and profiling. **Why This Matters:** Monitoring is essential for identifying and addressing performance issues. Identifying slow selectors, excessive rerenders, and other issues allows for targeted optimization. ### Standard 14: Write Targeted Tests **Do This:** Write unit tests to verify the behavior of your Zustand stores. Focus on testing state transitions and selector logic. **Don't Do This:** Neglect unit testing of the Zustand store logic. **Why This Matters:** Testing ensures that your Zustand stores function correctly and reliably. Writing specific tests for selectors and state transitions can prevent regressions and improve the overall quality of the application. ## 7. TypeScript Considerations ### Standard 15: Leverage TypeScript for Type Safety **Do This:** Use TypeScript to define precise types for your Zustand store's state and actions. Ensure that all actions are type-safe and handle edge cases appropriately. **Don't Do This:** Use "any" liberally or avoid defining types. This sacrifices the benefits of TypeScript and can lead to runtime errors. **Why This Matters:** TypeScript provides static type checking, which can help catch errors early in the development cycle, improving code quality and maintainability. **Code Example:** """typescript import { create } from 'zustand'; interface UserState { id: number; name: string; email: string; } interface UserActions { updateName: (name: string) => void; updateEmail: (email: string) => void; } const useUserStore = create<UserState & UserActions>((set) => ({ id: 1, name: 'John Doe', email: 'john.doe@example.com', updateName: (name: string) => set({ name }), updateEmail: (email: string) => set({ email }), })); export default useUserStore; """ By following these performance optimization standards, developers can create efficient and performant React applications leveraging the power of Zustand.
# Testing Methodologies Standards for Zustand This document outlines the coding standards for testing methodologies when using Zustand for state management in React applications. It aims to provide clear guidelines and best practices for unit, integration, and end-to-end testing, specifically tailored for Zustand's unique features and capabilities. Following these standards ensures maintainability, reliability, and performance of state management logic. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) * **Do This:** Embrace TDD by writing tests *before* implementing functionality. This helps to clarify requirements and ensures thorough testing from the outset. * **Don't Do This:** Neglect writing tests until after the implementation is complete. This often leads to incomplete or poorly designed tests. **Why:** TDD shifts the focus towards well-defined requirements and facilitates the construction of more testable and maintainable code. ### 1.2. Test Pyramid * **Do This:** Follow the structure of the test pyramid: more unit tests, fewer integration tests, and even fewer end-to-end tests. Unit tests should form the base, providing rapid feedback and excellent coverage of individual components and Zustand stores. * **Don't Do This:** Rely heavily on end-to-end tests at the expense of unit and integration tests. This makes debugging slower and more complex. **Why:** A balanced testing strategy ensures comprehensive coverage while optimizing for speed and maintainability. Unit tests are faster and more focused, while integration and E2E tests are more holistic but slower. ### 1.3. Test Naming Conventions * **Do This:** Use clear and descriptive test names. Use a standardized convention like "describe('ComponentName/Store Slice - Unit of Work', () => { ... });" or "it('should do something', () => { ... });". Consider BDD-style naming ("should do X when Y"). * **Don't Do This:** Use vague or ambiguous test names that don't clearly indicate the purpose of the test. Avoid names like "test1", "test2", etc. **Why:** Clear test names make it easier to understand the functionality being tested and quickly identify the cause of failures. ### 1.4. Test Isolation * **Do This:** Ensure tests are isolated from each other. Use mocking and stubbing judiciously to control dependencies and prevent tests from interfering with each other’s state. Zustand stores are particularly susceptible to state bleeding, so use "store.setState()" or a store reset function to clean up after each test. * **Don't Do This:** Allow tests to share state or rely on specific execution order. This can lead to flaky and unreliable test results. **Why:** Isolated tests are more reliable, reproducible, and easier to debug. ## 2. Unit Testing Zustand Stores ### 2.1. Store Structure and Testing Scope * **Do This:** Unit test each slice or logical unit of your Zustand store independently. This allows precise focus on individual state mutations and actions. Consider how the store is sliced and define test suites related to each slice. * **Don't Do This:** Create "god" tests that attempt to test the entire store in a single test case. This is brittle and difficult to maintain. **Why:** Focusing on individual slices promotes modularity and easier debugging. ### 2.2. Testing State Mutations * **Do This:** Verify that actions correctly modify the store's state. Use assertions to check the state before and after actions are called. * **Don't Do This:** Only focus on testing the return value of actions without validating the resulting state. **Why:** The primary responsibility of a Zustand store is to manage state effectively. Tests should validate state transitions. **Example:** """javascript import { create } from 'zustand'; import { describe , it, expect, beforeEach } from 'vitest'; // Or use Jest import { act } from 'react-dom/test-utils'; const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }) })); describe('Counter Store', () => { let store; beforeEach(() => { store = createCounterStore(); }); it('should initialize count to 0', () => { const { count } = store.getState(); expect(count).toBe(0); }); it('should increment the count', () => { act(() => { store.getState().increment(); }); const { count } = store.getState(); expect(count).toBe(1); }); it('should decrement the count', () => { act(() => { store.getState().decrement(); }); const { count } = store.getState(); expect(count).toBe(-1); }); it('should reset the count to 0', () => { act(() => { store.getState().increment(); store.getState().reset(); }); const { count } = store.getState(); expect(count).toBe(0); }); }); """ ### 2.3. Testing Actions and Side Effects * **Do This:** If actions trigger side effects (e.g., API calls), mock the dependencies to control the external environment. Use "jest.mock" (or equivalent in other testing frameworks) to mock API functions. Ensure actions correctly handle both success and error scenarios. * **Don't Do This:** Allow tests to make actual API calls, as this can lead to unpredictable results and slow test execution. **Why:** Mocking allows you to isolate the store's logic and simulate different scenarios without relying on external systems. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the API service const mockApiService = { fetchData: vi.fn() }; const createDataStore = () => create((set) => ({ data: null, loading: false, error: null, fetchData: async () => { set({ loading: true, error: null }); try { const data = await mockApiService.fetchData(); set({ data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } } })); describe('Data Store', () => { let store; beforeEach(() => { store = createDataStore(); }); it('should initialize with default values', () => { const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should set loading to true when fetching data', async () => { mockApiService.fetchData.mockResolvedValue({}); // Resolve immediately so loading changes. act(() => { // Wrap store changes. store.getState().fetchData(); }); expect(store.getState().loading).toBe(true); }); it('should fetch data successfully', async () => { const mockData = { name: 'Test Data' }; mockApiService.fetchData.mockResolvedValue(mockData); await act(async () => { await store.getState().fetchData(); // Wait for async function to complete. }); const { data, loading, error } = store.getState(); expect(data).toEqual(mockData); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should handle errors when fetching data', async () => { mockApiService.fetchData.mockRejectedValue(new Error('Failed to fetch')); await act(async () => { await store.getState().fetchData(); // Await promise. }); const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBe('Failed to fetch'); }); }); """ ### 2.4. Testing Selectors * **Do This:** If the store uses selectors to derive data, write unit tests to verify that the selectors return the correct values based on different store states. * **Don't Do This:** Assume that selectors always work correctly without dedicated testing. **Why:** Selectors are an important part of the store logic, and testing them ensures that derived data is accurate. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect } from 'vitest'; import { act } from 'react-dom/test-utils'; const createItemStore = () => create((set, get) => ({ items: [{ id: 1, name: 'Item 1', price: 10 }, { id: 2, name: 'Item 2', price: 20 }], totalPrice: () => { const items = get().items; return items.reduce((sum, item) => sum + item.price, 0); }, addItem: (item) => set(state => ({items: [...state.items, item]})) })); describe('Item Store', () => { it('should calculate the total price correctly', () => { const store = createItemStore(); const totalPrice = store.getState().totalPrice(); expect(totalPrice).toBe(30); }); it('should update total price when an item is added', () => { const store = createItemStore(); const initialTotalPrice = store.getState().totalPrice(); const newItem = { id: 3, name: "New Item", price: 5 }; act(() => { store.getState().addItem(newItem); }); const newTotalPrice = store.getState().totalPrice(); expect(newTotalPrice).toBe(initialTotalPrice + newItem.price); // 30 + 5 = 35 }); }); """ ### 2.5. Testing with Time * **Do This:** When dealing with asynchronous operations and timers (e.g., debouncing, throttling, timeouts), leverage tools like "jest.useFakeTimers()". This creates controlled and predictable tests. * **Don't Do This:** Rely on real-time delays in tests, which can make tests slow and unreliable. **Why:** Fake timers allow you to fast-forward time and reliably test asynchronous interactions. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { act } from 'react-dom/test-utils'; const createDebouncedStore = () => create(set => ({ value: '', setValue: vi.fn((newValue) => { // This vi.fn allows you to see if setValue has been called set({value: newValue}) }), // Mock setValue for testing purposes debouncedSetValue: (newValue) => { debounceSetValueInternal(newValue,set); } })); let debounceSetValueInternal = (newValue,set) => { return setTimeout(() => { set({ value: newValue }); }, 300); }; describe('Debounced Store', () => { beforeEach(() => { vi.useFakeTimers(); // Enable fake timers. Don't use modern; use a basic fake timer implementation if you intend to use setTimeout. }); afterEach(() => { vi.useRealTimers(); }); it('should update the value after the debounce delay', () => { const store = createDebouncedStore(); act(() => { store.getState().debouncedSetValue('test'); }); // Fast-forward time by 300ms vi.advanceTimersByTime(300); // Use advanceTimersByTime expect(store.getState().value).toBe('test'); }); }); """ ### 2.6 Zustand-Specific Hooks and Utilities * **Do This:** When testing components that use the Zustand store directly through hooks, test the behavior of effects, selectors, and actions initiated through the hook. * **Don't Do This:** Neglect to test how your components interact with the state in the store. **Why:** Zustand's hook-based approach encourages testing state transformations and side effects that are triggered in components. ## 3. Integration Testing with React Components ### 3.1. Component Interactions with Zustand * **Do This:** Test how React components interact with the Zustand store. Use testing libraries like React Testing Library to simulate user interactions and verify that the store is updated correctly. * **Don't Do This:** Focus solely on the component's rendering output without validating its integration with the store. **Why:** Integration tests bridge the gap between unit tests and end-to-end tests, verifying that components and the store work together as expected. **Example:** """javascript import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { create } from 'zustand'; import { describe, it, expect, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the Zustand store const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); function CounterComponent() { const [count, increment] = createCounterStore((state) => [state.count, state.increment]); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } describe('Counter Component', () => { beforeEach(() => { createCounterStore.setState({ count: 0 }); // Reset state before each test. Important b/c store can be shared. }); it('should display the initial count', () => { render(<CounterComponent />); const countElement = screen.getByText('Count: 0'); expect(countElement).toBeInTheDocument(); }); it('should increment the count when the button is clicked', async () => { const user = userEvent.setup() render(<CounterComponent />); const incrementButton = screen.getByText('Increment'); await user.click(incrementButton); const countElement = screen.getByText('Count: 1'); expect(countElement).toBeInTheDocument(); }); }); """ ### 3.2. Mocking Zustand for Isolated Component Tests * **Do This:** For component-specific tests where you want to isolate the component and avoid relying on the real Zustand store, mock the store or its hook usage. This can be achieved using "jest.mock" to replace the store's hook with a mock implementation. * **Don't Do This:** Unnecessarily test the Zustand store's internal logic within component tests. Component tests should primarily focus on the component's behavior given a specific state. **Why:** Mocking the store allows you to control the state injected into the component and test different scenarios in isolation. This is useful when the component interacts with a complex store and you only need to test a specific aspect of its behavior. ### 3.3. Testing Component Side Effects * **Do This:** Verify that components correctly trigger store actions in response to user interactions or lifecycle events. Spy functions (e.g., "jest.spyOn") can be used to assert that actions are called with the expected parameters. * **Don't Do This:** Neglect to test side effects triggered by components, as these are often a critical part of the application's functionality. **Why:** Components are responsible for initiating state changes, and testing these interactions ensures that the store is updated correctly in response to user actions. ## 4. End-to-End (E2E) Testing ### 4.1. Realistic User Flows * **Do This:** End-to-end tests should simulate complete user flows through the application, including interactions with the Zustand store. Use tools like Cypress or Playwright to automate these tests. * **Don't Do This:** Write E2E tests that only cover basic functionality or skip important user interactions. **Why:** E2E tests provide a high level of confidence that the application works correctly in a real-world environment. ### 4.2. State Validation in E2E Tests * **Do This:** Include assertions to validate that the Zustand store is updated correctly as users interact with the application. This can be done by querying the UI for updated values that reflect the store's state. * **Don't Do This:** Rely solely on visual checks without validating the underlying state changes. **Why:** Validating the store's state in E2E tests ensures that the application's data is consistent and that user interactions have the expected effect on the state. ### 4.3. E2E Test Environment * **Do This:** Set up a dedicated test environment for E2E tests, including seed data and mock services to ensure consistent and predictable results. * **Don't Do This:** Run E2E tests against production environments, which can be unpredictable and potentially disruptive. **Why:** Dedicated test environments provide a controlled environment for E2E tests, ensuring that they are reliable and reproducible. ## 5. Common Anti-Patterns * **Over-mocking:** Avoid mocking everything. Mocking too much can lead to tests that are too far removed from reality and don't catch actual integration issues. * **Ignoring edge cases:** Ensure tests cover edge cases and boundary conditions to prevent unexpected behavior. * **Flaky tests:** Address flaky tests immediately. Flaky tests undermine confidence in the test suite and make it difficult to identify actual issues. Retries can be a workaround, but the core issue must be resolved. * **Lack of cleanup:** Always clean up after tests, especially when dealing with Zustand stores. Reset the store's state or mock dependencies to prevent tests from interfering with each other. * **Testing implementation details:** Tests should focus on the behavior of the store and components, not on the implementation details. Avoid asserting on private variables or internal function calls. ## 6. Technology-Specific Considerations * **React Testing Library:** Encourages testing from the user's perspective, focusing on what the user sees and interacts with. Avoid implementation-specific selectors. * **Vitest/Jest:** Popular JavaScript testing frameworks with excellent support for mocking, spying, and asynchronous testing. Use "jest.mock" for mocking dependencies. Vitest is generally faster for Vite-based projects; Jest is more widely known and used. * **Cypress/Playwright:** Powerful E2E testing tools that allow you to automate browser interactions and validate the application's behavior. By adhering to these coding standards, development teams can create robust and reliable Zustand-based applications with confidence, ensuring that state management logic is thoroughly tested and maintainable.