# Deployment and DevOps Standards for Zustand
This document outlines the coding standards for Zustand specifically concerning deployment and DevOps practices. These guidelines aim to ensure maintainable, performant, and secure state management in production environments utilizing Zustand.
## 1. Build Processes, CI/CD, and Production Considerations for Zustand
### 1.1. Standards
* **DO** utilize environment variables to configure Zustand stores for different environments (development, staging, production).
* **WHY:** Environment variables allow for dynamic configuration without modifying code, separating configuration from code, enhancing portability, and improving security by avoiding hardcoding sensitive information.
* **DON'T** hardcode environment-specific values directly into Zustand store definitions.
* **WHY:** This tightly couples the code to specific environments, making it difficult to deploy or maintain and increases the risk of errors when transitioning between environments.
* **DO** integrate Zustand store initialization into your CI/CD pipeline using build-time or runtime configuration.
* **WHY:** Automating the configuration process ensures consistency across deployments and reduces the risk of manual configuration errors.
* **DON'T** rely on manual configuration of Zustand stores in production.
* **WHY:** Manual configuration is prone to errors, difficult to track, and inconsistent.
* **DO** implement feature flags to enable or disable Zustand-related functionality without redeploying code.
* **WHY:** Feature flags provide a mechanism for gradual rollouts, A/B testing, and emergency shutdowns, which reduces risk and improves agility.
* **DON'T** deploy breaking changes to Zustand store structures without proper migration strategies and feature flags.
* **WHY:** Breaking changes can lead to application errors and data loss. Feature flags allow for controlled and gradual rollouts, mitigating the impact of breaking changes.
* **DO** use proper semantic versioning to track changes to your Zustand stores and their interactions/dependencies within components.
* **WHY:** Semantic Versioning (SemVer) allows for clarity across teams when deploying or referencing code that relies on the store, to minimize potential errors.
### 1.2. Code Examples
"""typescript
// Example: Using environment variables to configure Zustand store
import { create } from 'zustand';
interface MyStoreState {
apiEndpoint: string;
featureXEnabled: boolean;
}
interface MyStoreActions {
initialize: () => void;
}
type MyStore = MyStoreState & MyStoreActions;
const useMyStore = create((set) => ({
apiEndpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://default-api.com', // Utilize env variable
featureXEnabled: process.env.NEXT_PUBLIC_FEATURE_X === 'true', // Utilize env variable
initialize: () => {
// Any initialization logic that depends on the configuration
console.log("Store Initialized with:", {
apiEndpoint: process.env.NEXT_PUBLIC_API_ENDPOINT,
featureXEnabled: process.env.NEXT_PUBLIC_FEATURE_X
})
},
}));
// Component using the store
function MyComponent() {
const apiEndpoint = useMyStore((state) => state.apiEndpoint);
const featureXEnabled = useMyStore((state) => state.featureXEnabled);
const initialize = useMyStore((state) => state.initialize);
useEffect(() => {
initialize();
}, [initialize]);
return (
<p>API Endpoint: {apiEndpoint}</p>
<p>Feature X Enabled: {featureXEnabled ? 'Yes' : 'No'}</p>
{/* Component Logic based on featureXEnabled */}
);
}
export default MyComponent;
// .env.production (Example)
// NEXT_PUBLIC_API_ENDPOINT=https://production-api.com
// NEXT_PUBLIC_FEATURE_X=true
"""
### 1.3. Anti-Patterns
* **Direct modification of the state outside of the store's "set":**
* Directly modifying the state bypasses Zustand's change tracking and can lead to unexpected behavior in production.
* **Storing sensitive data directly in the Zustand store, especially client-side:**
* Sensitive data in client-side Zustand stores can be exposed to users or malicious actors. Always handle sensitive data securely on the server-side.
* **Not handling errors gracefully when updating the Zustand store:**
* Unhandled errors can lead to the store not updating correctly, resulting in inconsistent data and application failures.
### 1.4. Performance Considerations
* **Optimize Zustand store updates to minimize unnecessary re-renders:**
* Use "useMemo" or "useCallback" to prevent unnecessary re-renders when values from Zustand store is used.
* **Lazy-load Zustand stores or initialize them only when needed:**
* Initializing stores on demand can significantly improve initial load times, especially in large applications.
## 2. Monitoring and Logging for Zustand
### 2.1. Standards
* **DO** implement logging for Zustand store updates, especially in production environments.
* **WHY:** Logging provides valuable insights into the application's state and helps identify and diagnose issues related to state management.
* **DON'T** log sensitive data or personally identifiable information (PII) in your Zustand store logs.
* **WHY:** Logging sensitive data can violate privacy regulations and expose users to risk.
* **DO** use structured logging formats (e.g., JSON) for Zustand store logs to facilitate analysis and querying.
* **WHY:** Structured logging allows for easier filtering, aggregation, and analysis of log data.
* **DON'T** rely solely on "console.log" for production logging.
* **WHY:** "console.log" is not suitable for production logging due to its lack of structure, filtering capabilities, and integration with centralized logging systems.
* **DO** integrate Zustand store monitoring with existing application monitoring tools (e.g., Datadog, New Relic).
* **WHY:** Centralized monitoring provides a holistic view of application health and performance, including the Zustand store.
* **DON'T** ignore Zustand store-related errors or warnings in your monitoring system.
* **WHY:** Errors and warnings can indicate potential issues with the Zustand store and should be investigated promptly.
### 2.2. Code Examples
"""typescript
// Example: Logging Zustand store updates
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
interface MyStoreState {
count: number;
increment: () => void;
}
const useMyStore = create()(
devtools((set) => ({
count: 0,
increment: () => {
set((state) => {
const newState = { count: state.count + 1 };
console.log('Zustand Store Update:', { action: 'increment', newState }); // Logging with context
return newState;
});
},
}))
);
// .env.development
// Enable logging by setting DISABLE_ZUSTAND_DEVTOOLS to false or undefined for development env
function MyComponent() {
const count = useMyStore((state) => state.count);
const increment = useMyStore((state) => state.increment);
return (
<p>Count: {count}</p>
Increment
);
}
export default MyComponent;
"""
### 2.3. Anti-Patterns
* **Logging overly verbose data in production:** This can negatively impact performance and increase storage costs. Only log essential information.
* **Not setting up log rotation or retention policies:** This can lead to log files growing excessively and consuming valuable disk space.
* **Ignoring error logs related to Zustand store updates:** This can result in undetected issues and application failures.
### 2.4. Security Considerations
* **Sanitize data before logging:** Remove or mask sensitive information to prevent accidental exposure.
* **Secure access to log files:** Restrict access to authorized personnel only and protect log files from unauthorized access.
* **Regularly review logs for security vulnerabilities and suspicious activity:** This can help detect and prevent potential security breaches.
## 3. Testing and Quality Assurance for Zustand
### 3.1. Standards
* **DO** write unit tests for Zustand store actions and selectors.
* **WHY:** Unit tests ensure that the store's logic behaves as expected and prevent regressions.
* **DON'T** neglect to test Zustand store interactions with components.
* **WHY:** Testing component interactions verifies that the store updates correctly trigger re-renders and that components receive the correct data.
* **DO** use mocking libraries to isolate Zustand stores during testing.
* **WHY:** Mocking allows you to control the store's state and behavior during testing, making tests more predictable and reliable.
* **DON'T** rely solely on manual testing for Zustand store functionality.
* **WHY:** Manual testing is time-consuming, error-prone, and not scalable for complex applications.
* **DO** integrate Zustand store testing into your CI/CD pipeline.
* **WHY:** Automated testing ensures that changes to the Zustand store do not introduce regressions.
* **DON'T** deploy code with failing Zustand store tests.
* **WHY:** Failing tests indicate potential issues with the store's logic and should be addressed before deployment.
### 3.2. Code Examples
"""typescript
// Example: Unit testing Zustand store actions
import { create } from 'zustand';
import { act } from 'react-dom/test-utils'; // Ensure you have react-dom installed
import { renderHook } from '@testing-library/react-hooks';
interface MyStoreState {
count: number;
increment: () => void;
decrement: () => void;
}
const useMyStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
describe('MyStore', () => {
it('should initialize count to 0', () => {
const { result } = renderHook(() => useMyStore());
expect(result.current.count).toBe(0);
});
it('should increment count', () => {
const { result } = renderHook(() => useMyStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count', () => {
const { result } = renderHook(() => useMyStore());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
});
"""
### 3.3. Anti-Patterns
* **Writing brittle tests that are tightly coupled to implementation details:** This makes tests difficult to maintain and prone to failure when refactoring code.
* **Not testing edge cases or error conditions in Zustand store actions:** This can lead to unexpected behavior in production.
### 3.4. Monitoring and Alerting
* **Set up alerts for failing tests in your CI/CD pipeline:** This allows you to quickly identify and address issues with the Zustand store.
* **Track code coverage for Zustand store tests:** This provides a metric for assessing the completeness of your testing strategy.
## 4. Security Best Practices Specific to Zustand
### 4.1 Standards
* **DO** sanitize any user-provided data before storing it in the Zustand store.
* **WHY:** Prevents XSS attacks and other security vulnerabilities.
* **DON'T** store sensitive information, such as passwords or API keys, directly in the Zustand store, especially client-side.
* **WHY:** Such data can be easily accessed by malicious actors. Use secure storage mechanisms like HTTPS-only cookies or browser's secure storage or server-side sessions.
* **DO** validate data being stored in the store to prevent unexpected data types or values from causing errors.
* **WHY:** Prevents unexpected errors or potential security vulnerabilities.
* **DON'T** expose the entire Zustand store to external libraries or components unnecessarily.
* **WHY:** This can increase the attack surface of your application. Only expose the specific data or actions that are required.
### 4.2 Code Examples
"""typescript
// Example: Sanitizing user input before storing in Zustand
import { create } from 'zustand';
interface UserInputStoreState {
userInput: string;
updateUserInput: (input: string) => void;
}
//Basic Sanitization Function
const sanitizeInput = (input: string) => {
// Remove any HTML tags
let cleanInput = input.replace(/<[^>]*>/g, '');
// Encode HTML entities
cleanInput = cleanInput.replace(/[&<>"']/g, (m) => {
switch (m) {
case '&':
return '&';
case '<':
return '<';
case '>':
return '>';
case '"':
return '"';
case "'":
return ''';
default:
return m;
}
});
return cleanInput;
};
const useUserInputStore = create((set) => ({
userInput: '',
updateUserInput: (input: string) => set({ userInput: sanitizeInput(input) }), // Sanitize
}));
"""
### 4.3 Anti-Patterns
* **Trusting data received from external sources, such as APIs, without validation:** This can lead to the store being populated with malicious or incorrect data.
* **Using "eval()" or similar functions to execute code stored in the Zustand store:** This can create significant security vulnerabilities.
### 4.4 Deployment and DevOps
* **Implement regular security audits of your Zustand store and its interactions with other components:** This can help identify and address potential security vulnerabilities.
* **Keep Zustand and its dependencies up to date with the latest security patches:** This can protect against known vulnerabilities.
These guidelines provide a comprehensive set of standards for deployment, DevOps, monitoring, testing, and security best practices when using Zustand. By adhering to these standards, development teams can ensure the reliability, performance, and security of their applications while simplifying maintenance and reducing risk in production environments.
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.