# Tooling and Ecosystem Standards for Zustand
This document outlines the standards for tooling and ecosystem components when developing applications using Zustand. It provides guidelines for selecting and integrating libraries, tools, and extensions to enhance development workflows, improve code quality, and optimize performance.
## 1. Recommended Libraries and Tools
### 1.1. Devtools Integration
**Do This:** Integrate "zustand/middleware"'s "devtools" middleware for enhanced debugging capabilities.
**Don't Do This:** Skip devtools integration, especially in development environments. This makes debugging significantly harder.
**Why:** Devtools provide a time-travel debugging experience, allowing developers to inspect state changes and identify issues more efficiently.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
const useStore = create(devtools((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
})));
export default useStore;
"""
**Anti-Pattern:** Manually logging state changes. Devtools provide a structured and interactive way to observe these changes. Also, avoid leaving "devtools" enabled in production builds.
**Technology-Specific Details:** The "devtools" middleware connects to the Redux DevTools extension (available for Chrome, Firefox, and Edge). Ensure the extension is installed and enabled in your browser.
### 1.2. Persist Middleware
**Do This:** Utilize "zustand/middleware"'s "persist" middleware for persisting store data to local storage or other storage mechanisms.
**Don't Do This:** Implement custom persistence solutions. This increases the risk of errors and inconsistencies.
**Why:** Persistence ensures that application state is preserved across sessions, providing a better user experience.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const useStore = create(persist((set) => ({
bears: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
}), {
name: 'bear-storage', // unique name
getStorage: () => localStorage, // (optional) by default, 'localStorage' is used
}));
export default useStore;
"""
**Anti-Pattern:** Storing sensitive data in local storage without encryption. Consider using "redux-persist-transform-encrypt" or similar libraries to encrypt data. Also, avoid storing large amounts of data in local storage that could impact performance.
**Technology-Specific Details:** The "persist" middleware allows customization of the storage mechanism (e.g., "localStorage", "sessionStorage", cookies, IndexedDB). Provide a custom "getStorage" function to use alternative storage solutions. Be mindful of storage limits.
### 1.3. Immer Integration
**Do This:** Use "immer" to simplify immutable state updates, especially for complex state objects. Zustand itself doesn't require Immer, but its integration often leads to cleaner code. Consider using "set(produce(state => { ... }))" with Zustand, it already exports the produce function.
**Don't Do This:** Manually create new state objects for every update when dealing with deeply nested state.
**Why:** Immer allows you to work with mutable drafts of your state, which are then automatically converted to immutable updates, reducing boilerplate and improving code readability.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { produce } from 'immer';
const useStore = create((set) => ({
user: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'Anytown',
},
},
updateCity: (newCity) => set(produce((state) => {
state.user.address.city = newCity;
})),
}));
export default useStore;
"""
**Anti-Pattern:** Overusing Immer for simple state updates. For simple updates like incrementing a counter, direct state updates are more efficient.
**Technology-Specific Details:** Ensure "immer" is installed as a project dependency ("npm install immer" or "yarn add immer").
### 1.4. Zustand Selectors with "useRef"
**Do This:** Use selectors for derived state, and memoize them effectively using "useRef" to prevent unnecessary re-renders.
**Don't Do This:** Perform complex calculations directly within the component's render function. This can lead to performance issues.
**Why:** Selectors allow you to efficiently derive state from the store and only re-render components when the derived state changes. "useRef" provides a way to memoize the result of a selector, further optimizing performance.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { useRef } from 'react';
import { shallow } from 'zustand/shallow';
const useStore = create((set) => ({
items: [{ id: 1, price: 10 }, { id: 2, price: 20 }],
discount: 0.1,
setDiscount: (discount) => set({ discount }),
}));
const useTotal = () => {
const totalRef = useRef<(state: any) => number | null>(null);
if (!totalRef.current) {
totalRef.current = (state) => {
if (!state.items || state.items.length === 0) return null;
const subtotal = state.items.reduce((acc, item) => acc + item.price, 0);
return subtotal * (1 - state.discount);
}
}
return useStore(totalRef.current, shallow);
}
function MyComponent() {
const total = useTotal();
return (
Total: {total}
);
}
"""
**Anti-Pattern:** Creating new selector objects on every render. This defeats the purpose of memoization.
**Technology-Specific Details:** The "shallow" comparison function from "zustand/shallow" is helpful for preventing re-renders when only a nested property within the state object changes.
### 1.5 Computed Properties with Selectors and "useMemo"
**Do This:** Utilize "useMemo" in conjunction with Zustand selectors for complex computed properties to optimize performance and prevent unnecessary recalculations.
**Don't Do This:** Perform expensive computations directly within your components without memoization. This will cause performance issues, especially with frequently updating stores. Avoid creating functions inside the components, this will break the optimization.
**Why:** Memoization ensures that computed properties are only recalculated when their dependencies change, improving component rendering performance. This approach leverages React's "useMemo" hook to cache the result of a computation, making it efficient and performant.
**Code Example:**
"""jsx
import { create } from 'zustand';
import { useMemo } from 'react';
const useStore = create((set) => ({
items: [
{ id: 1, name: 'Apple', price: 1 },
{ id: 2, name: 'Banana', price: 0.5 },
{ id: 3, name: 'Orange', price: 0.75 },
],
taxRate: 0.1,
setTaxRate: (newRate) => set({ taxRate: newRate }),
}));
function useCalculatedTotal() {
const items = useStore(state => state.items);
const taxRate = useStore(state => state.taxRate);
const calculatedTotal = useMemo(() => {
console.log('Recalculating total'); // For demonstration
const subtotal = items.reduce((acc, item) => acc + item.price, 0);
const taxAmount = subtotal * taxRate;
return subtotal + taxAmount;
}, [items, taxRate]);
return calculatedTotal;
}
function ShoppingCart() {
const total = useCalculatedTotal();
const setTaxRate = useStore(state => state.setTaxRate);
return (
<p>Total: ${total.toFixed(2)}</p>
setTaxRate(0.2)}>Increase Tax Rate
);
}
export default ShoppingCart;
"""
**Anti-Pattern:** Forgetting to include all dependencies in the "useMemo" dependency array. This can lead to stale values and incorrect computations. Also, recalculation will always happen on initial render.
**Technology-Specific Details:** This approach is particularly useful when the calculation involves complex logic or large datasets. Using "useMemo" ensures that the calculation is only performed when necessary, preventing performance bottlenecks.
### 1.6. Immer Integration with Redux DevTools
**Do This:** Combine "immer" and Redux DevTools for powerful debugging and state management when using Zustand
**Don't Do This:** Avoid using DevTools when using Immer with Zustand. This leads to difficulties in debugging state transformation.
**Why:** This combination allows you to visualize each state mutation with Immer while leveraging the time-travel debugging features of the DevTools. It helps track the precise evolution of the state, greatly simplifying debugging complex applications.
**Code Example:**
"""javascript
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware';
const useStore = create(devtools(immer((set) => ({
counter: 0,
increment: () => set((state) => {
state.counter += 1; // Immer allows direct mutation here
}),
decrement: () => set((state) => {
state.counter -= 1; // Immer handles immutability under the hood
}),
}))));
export default useStore;
"""
**Anti-Pattern:** Applying Immer without using DevTools. This deprives you of the ability to visualize the exact mutations performed via Immer, increasing debugging complexity.
**Technology-Specific Details:** Immer enhances the debugging experience in Redux DevTools, enabling developers to inspect the state before and after each mutation step, making it easier to pinpoint the source of issues
### 1.7 Testing with Zustand
**Do This:** Unit test your store logic thoroughly, mocking dependencies as necessary. Zustand stores are highly testable due to their simple, functional nature.
**Don't Do This:** Neglect store testing. Untested stores can lead to unexpected behavior and difficult-to-debug issues. Also, avoid testing implementation details. Focus on testing the public API of your store (actions and selectors).
**Why:** Testing ensures the correctness and reliability of your state management logic.
**Code Example (using Jest and @testing-library/react-hooks):**
"""javascript
import { create } from 'zustand';
import { act, renderHook } from '@testing-library/react-hooks';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
describe('useStore', () => {
it('should initialize count to 0', () => {
const { result } = renderHook(() => useStore());
expect(result.current.count).toBe(0);
});
it('should increment count when increment is called', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement count when decrement is called', () => {
const { result } = renderHook(() => useStore());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
});
"""
**Anti-Pattern:** Testing the internal implementation of the store. Tests should focus on the public API (actions and selectors). Also, avoid tightly coupling tests to specific store implementation details.
**Technology-Specific Details:** Use "@testing-library/react-hooks" for testing Zustand stores in isolation, without rendering React components. "act" is crucial for wrapping state updates to ensure they are properly batched. Consider using Jest for mocking and assertions.
### 1.8 Zustand with TypeScript
**Do This:** Leverage TypeScript to define store state, actions, and selectors with strong types. This improves code maintainability and reduces runtime errors.
**Don't Do This:** Skip TypeScript usage in Zustand stores, especially for complex applications. This increases the risk of type-related errors and reduces code clarity.
**Why:** TypeScript provides static type checking, which helps catch errors early in the development process.
**Code Example:**
"""typescript
import { create } from 'zustand';
interface BearState {
bears: number;
increaseBears: () => void;
}
const useBearStore = create((set) => ({
bears: 0,
increaseBears: () => set((state) => ({ bears: state.bears + 1 })),
}));
export default useBearStore;
"""
**Anti-Pattern:** Using "any" type for store state. This defeats the purpose of using TypeScript. Provide specific type definitions for your state and actions.
**Technology-Specific Details:** Define interfaces or types for your state and actions. Use generic types to create reusable store definitions.
### 1.9 Zustand and React Server Components (RSCs)
**Do This**: Exercise extreme caution when mixing Zustand with React Server Components, especially in Next.js 13 and above. Understand the limitations described in the Zustand GitHub repository (see #2200). Prefer alternative data fetching and state management strategies for server-side data.
**Don't Do This**: Directly initialize or mutate Zustand stores within React Server Components unless you fully understand the implications for data privacy and potential bugs related to shared server state.
**Why**: Zustand is primarily designed for client-side state management. Incorrectly using it within RSCs can lead to unexpected behavior, data leakage between users, and difficulties in debugging due to the server-side execution context.
**Code Example (Illustrating a potential problem - AVOID THIS PATTERN unless you have a deep understanding of the consequences):**
"""javascript
// This is an ANTI-PATTERN for Next.js 13+ Server Components!
// Use with EXTREME CAUTION!
// NOT RECOMMENDED
import { create } from 'zustand';
const useStore = create((set) => ({
serverData: 'Initial Value',
updateServerData: (newValue) => set({ serverData: newValue }),
}));
//This component runs on the server! The store is shared across requests!
export default function MyServerComponent() {
const data = useStore((state) => state.serverData);
const updateData = useStore((state) => state.updateServerData);
return (
<p>Server Data: {data}</p>
updateData('New Value')}>Update Data
);
}
"""
**Anti-Pattern**: Assuming that Zustand will behave identically in RSCs as it does in client components. The server-side environment introduces complexities related to state persistence and sharing across requests. Relying on client-side initialization patterns will likely break.
**Technology-Specific Details**: In Next.js 13+, consider using server actions, route handlers, and alternative state management solutions like React Context with data fetched from a database or API when working with server components. Isolate client-side state to client components using the "'use client'" directive and initialize Zustand stores there. Thoroughly test any integration of Zustand with RSCs in a production-like environment.
## 2. Code Formatting and Linting
### 2.1. Consistent Code Style
**Do This:** Use a code formatter like Prettier to enforce consistent code style across the project.
**Don't Do This:** Rely on manual code formatting. This can lead to inconsistencies and increase code review time.
**Why:** Consistent code style improves code readability and maintainability.
**Code Example (.prettierrc.js):**
"""javascript
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
tabWidth: 2,
};
"""
**Anti-Pattern:** Inconsistent indentation, spacing, and line breaks.
**Technology-Specific Details:** Integrate Prettier with your editor for automatic code formatting on save. Use a linter like ESLint to enforce code quality rules and catch potential errors.
### 2.2. ESLint Configuration
**Do This:** Configure ESLint with recommended rules for React and JavaScript/TypeScript. Consider using the "eslint-plugin-react-hooks" plugin to enforce rules of hooks.
**Don't Do This:** Disable important ESLint rules. While customization is possible, avoid disabling rules that enforce best practices.
**Why:** ESLint helps identify potential issues in your code and enforce coding standards.
**Code Example (.eslintrc.js):**
"""javascript
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
plugins: ['react', '@typescript-eslint'],
rules: {
// Add or override rules here
},
settings: {
react: {
version: 'detect',
},
},
};
"""
**Anti-Pattern:** Ignoring ESLint warnings and errors. Address these issues to improve code quality.
**Technology-Specific Details:** Integrate ESLint with your editor for real-time code analysis. Use a pre-commit hook (e.g., with Husky and lint-staged) to ensure that all code passes linting checks before being committed.
## 3. Performance Optimization
### 3.1. Minimizing Re-renders
**Do This:** Use selectors and "shallow" comparison to prevent unnecessary re-renders.
**Don't Do This:** Connect components directly to the store's entire state. This causes re-renders even when only a small part of the state changes.
**Why:** Reducing re-renders improves application performance and responsiveness.
**Code Example:** See section 1.4 for example usage
**Anti-Pattern:** Passing the entire store state as props to child components. Use selectors to pass only the necessary data.
### 3.2. Batching Updates
**Do This:** Utilize "React.batch" or equivalent mechanisms to batch multiple state updates into a single re-render.
**Don't Do This:** Trigger multiple state updates within a short time period without batching. This can lead to performance issues.
**Why:** Batching reduces the number of re-renders, improving performance.
**Code Example:**
"""javascript
import { unstable_batchedUpdates } from 'react-dom'; // react-dom version >= 18
const updateMultipleValues = () => {
unstable_batchedUpdates(() => {
useStore.setState({ value1: 'new value 1' });
useStore.setState({ value2: 'new value 2' });
});
};
"""
**Anti-Pattern:** Manually managing update queues without leveraging "unstable_batchedUpdates".
**Technology-Specific Details:** "unstable_batchedUpdates" is available in "react-dom". Consider using "ReactDOM.unstable_batchedUpdates" if not using React 18 or later.
## 4. Security Considerations
### 4.1. Data Sanitization
**Do This:** Sanitize user input before storing it in the store to prevent XSS attacks.
**Don't Do This:** Store unsanitized user input directly in the store. This can create security vulnerabilities.
**Why:** Sanitization helps prevent malicious code from being injected into your application.
**Code Example:**
"""javascript
import { create } from 'zustand';
import DOMPurify from 'dompurify';
const useStore = create((set) => ({
userInput: '',
updateUserInput: (input) => set({ userInput: DOMPurify.sanitize(input) }),
}));
export default useStore;
"""
**Anti-Pattern:** Storing sensitive information without proper encryption. Also, passing the whole state object without validation.
### 4.2. Preventing State Tampering
**Do This:** Protect your store from unauthorized modifications. Consider using middleware to validate state updates.
**Don't Do This:** Allow arbitrary modifications to the store without validation. This can compromise the integrity of your application.
**Why:** Protecting the store from unauthorized modifications ensures that your application behaves as expected.
Remember to always refer to the [official Zustand documentation](https://github.com/pmndrs/zustand) for the most up-to-date information and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Code Style and Conventions Standards for Zustand This document outlines the code style and conventions standards for Zustand, a minimalist state management library for React applications. Adhering to these standards promotes consistency, readability, maintainability, and performance across our projects. These guidelines are designed for the latest versions of Zustand and incorporate modern best practices. ## 1. General Formatting and Style Maintaining a consistent code style is crucial for readability and collaboration. These standards apply to all code within Zustand-managed components and stores. ### 1.1. Formatting Tools * **Do This:** Use Prettier and ESLint to automatically format and lint your code. Configure them with a shared configuration file across the team to ensure consistency. * **Don't Do This:** Rely on manual formatting or inconsistent editor settings. **Why:** Automated formatting eliminates subjective style debates and ensures consistent code appearance across the project. It prevents style-related distractions during code reviews. **Example:** """javascript // .prettierrc.js module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; // .eslintrc.js module.exports = { extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], plugins: ['react', 'prettier'], parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true, }, }, rules: { 'prettier/prettier': 'error', 'react/prop-types': 'off', // Consider enabling for specific projects }, settings: { react: { version: 'detect', }, }, env: { browser: true, node: true, es6: true, }, }; """ ### 1.2. Indentation and Spacing * **Do This:** Use 2 spaces for indentation. * **Do This:** Maintain consistent spacing around operators, after commas, and within curly braces. **Why:** Consistent indentation and spacing improve readability and reduce visual clutter. **Example:** """javascript // Correct const myVariable = { prop1: value1, prop2: value2 }; const result = a + b * c; // Incorrect const myVariable={prop1:value1,prop2:value2}; const result=a+b*c; """ ### 1.3. Line Length * **Do This:** Limit lines to a maximum of 120 characters. * **Do This:** Break long lines into multiple shorter lines to improve readability. **Why:** Shorter lines are easier to read, especially on smaller screens or when code is displayed side-by-side. **Example:** """javascript // Correct const veryLongVariableName = someFunction( argument1, argument2, argument3, ); // Incorrect const veryLongVariableName = someFunction(argument1, argument2, argument3); """ ### 1.4. Comments * **Do This:** Write clear and concise comments to explain complex logic or non-obvious code. * **Do This:** Use JSDoc-style comments to document functions, components, and variables. * **Don't Do This:** Write obvious or redundant comments that describe what the code is already doing. **Why:** Comments improve code maintainability and help other developers (including your future self) understand the code's purpose and functionality. **Example:** """javascript /** * Calculates the total price of items in the cart. * * @param {Array<Object>} cartItems An array of cart items. * @returns {number} The total price. */ const calculateTotalPrice = (cartItems) => { // Reduce the cart items array to calculate the total price. return cartItems.reduce((total, item) => total + item.price * item.quantity, 0); }; """ ## 2. Zustand Specific Conventions These conventions apply specifically to Zustand stores and the code that interacts with them. ### 2.1. Store Definition * **Do This:** Define stores using the "create" function from Zustand. * **Do This:** Keep store definitions concise and focused on the state and actions that modify it. * **Do This:** Organize actions within the store for clarity. **Why:** The "create" function provides a consistent and idiomatic way to define Zustand stores. Well-organized stores are easier to understand and maintain. **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; """ ### 2.2. Naming Conventions * **Do This:** Use the "use" prefix for custom hooks that consume Zustand stores (e.g., "useBearStore"). * **Do This:** Name store properties and actions descriptively and consistently. Use camelCase. * **Do This:** For asynchronous actions, consider using a "fetch" or "load" prefix (e.g., "fetchUserData", "loadProducts"). **Why:** Consistent naming conventions improve code discoverability and make it easier to understand the purpose of different parts of the code. **Example:** """javascript import { create } from 'zustand'; const useUserStore = create((set) => ({ userData: null, isLoading: false, error: null, fetchUserData: async (userId) => { set({ isLoading: true, error: null }); try { const response = await fetch("/api/users/${userId}"); const data = await response.json(); set({ userData: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useUserStore; """ ### 2.3. Immutability * **Do This:** Update state immutably within Zustand stores. This is handled inherently by Zustand's "set" function, but be mindful when dealing with nested objects or arrays. * **Don't Do This:** Directly modify the state object. **Why:** Immutability ensures predictable state updates and prevents unexpected side effects. Zustand uses shallow equality checks, so it expects immutable updates. Modifying state directly will lead to components not re-rendering when they should. **Example:** """javascript // Correct (using the functional update form) set(state => ({ items: [...state.items, newItem] })); // or Immer import { create } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; const useStore = create(immer((set) => ({ items: [], addItem: (newItem) => set((state) => { state.items.push(newItem); // Immer handles immutability }), }))); // Incorrect (direct mutation) //state.items.push(newItem); // Don't do this! """ ### 2.4. Selectors * **Do This:** Use selectors to derive specific values from the store. * **Do This:** Memoize selectors to prevent unnecessary re-renders. You can use "useMemo" or libraries like "reselect". **Why:** Selectors optimize performance by preventing components from re-rendering when the specific data they depend on hasn't changed. **Example:** """javascript import useBearStore from './useBearStore'; import { useMemo } from 'react'; function BearCounter() { const bears = useBearStore((state) => state.bears); // Direct selection const doubleBears = useMemo(() => bears * 2, [bears]); // Memoized selector return <h1>{doubleBears}</h1>; } """ ### 2.5. Middleware * **Do This:** Use Zustand middleware to add common functionality like persistence, logging, or Immer integration. * **Do This:** Keep middleware functions modular and reusable. * **Don't Do This:** Implement complex logic directly within the store definition if it can be separated into middleware. **Why:** Middleware promotes separation of concerns and makes it easier to add cross-cutting functionality to Zustand stores. **Example:** """javascript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; const useBoundStore = create( persist( (set) => ({ fishes: 0, addAFish: () => set((state) => ({ fishes: state.fishes + 1 })), }), { name: 'food-storage', // unique name for the storage getStorage: () => localStorage, // (optional) by default the 'localStorage' is used }, ), ); export default useBoundStore; """ ### 2.6. Slices * **Do This:** Use slices for large stores. Slices make your store more manageable and testable * **Do This:** Group related state and actions into individual slices. * **Don't Do This:** Create extremely large stores without considering how to organize them. **Why:** Slices improve the organization of large stores by breaking them down into smaller, more manageable units. **Example:** """javascript import { create } from 'zustand'; const createCounterSlice = (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }); const createUserSlice = (set) => ({ user: null, setUser: (user) => set({ user }), }); const useStore = create((...a) => ({ ...createCounterSlice(...a), ...createUserSlice(...a), })); export default useStore; """ ### 2.7. Async Actions * **Do This:** Handle loading and error states within asynchronous actions. * **Do This:** Use "try...catch" blocks to handle errors gracefully. * **Do This:** Name asynchronous actions to clearly indicate their purpose. **Why:** Proper error handling and loading state management provide a better user experience and prevent unexpected behavior. **Example:** """javascript import { create } from 'zustand'; const useProductsStore = create((set) => ({ products: [], isLoading: false, error: null, fetchProducts: async () => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/products'); const data = await response.json(); set({ products: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useProductsStore; """ ### 2.8 Type Safety with TypeScript * **Do This**: Use TypeScript to define the state type for stores * **Do This**: Ensure all action definitions are properly typed * **Do This**: Create a separate type definition file for complex state structures **Why**: TypeScript provides type safety, improves code maintainability and provides better intellisense inside your IDE **Example**: """typescript import { create } from 'zustand' interface BearState { bears: number increasePopulation: () => void removeAllBears: () => void } const useBearStore = create<BearState>((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) export default useBearStore """ ## 3. Component Integration These standards focus on how Zustand stores are used within React components. ### 3.1. Selective Updates * **Do This:** Use the selector function provided by Zustand to subscribe only to the parts of the state that the component needs. * **Don't Do This:** Subscribe to the entire store if the component only needs a small piece of data. **Why:** Selective updates prevent unnecessary re-renders and improve component performance. **Example:** """javascript import useBearStore from './useBearStore'; function BearCounter() { const bears = useBearStore((state) => state.bears); // Select only the 'bears' property return <h1>{bears}</h1>; } """ ### 3.2. Avoiding Prop Drilling * **Do This:** Use Zustand to avoid prop drilling, especially for deeply nested components. * **Don't Do This:** Pass state and actions through multiple levels of components if they are only needed in a specific child component. **Why:** Zustand provides a centralized state management solution that eliminates the need to pass props through multiple levels of the component tree. **Example:** """javascript // Before (prop drilling) function Grandparent({ bears, increaseBears }) { return <Parent bears={bears} increaseBears={increaseBears} />; } function Parent({ bears, increaseBears }) { return <Child bears={bears} increaseBears={increaseBears} />; } function Child({ bears, increaseBears }) { return ( <> <h1>{bears} Bears</h1> <button onClick={increaseBears}>Increase</button> </> ); } // After (using Zustand) import useBearStore from './useBearStore'; function Grandparent() { return <Parent />; } function Parent() { return <Child />; } function Child() { const { bears, increaseBears } = useBearStore(); return ( <> <h1>{bears} Bears</h1> <button onClick={increaseBears}>Increase</button> </> ); } """ ### 3.3 Extracting Logic * **Do This:** Place complex logic inside the store definition. * **Don't Do This:** Implement complex logic inside components that consume Zustand stores if it is related to state updates. **Why:** Extracting logic keeps components clean and focused on rendering, making them easier to understand and test. **Example:** """javascript //Complex logic inside the store import { create } from 'zustand'; const useCartStore = create((set) => ({ cartItems: [], addItemToCart: (item) => set((state) => { const existingItem = state.cartItems.find((cartItem) => cartItem.id === item.id); if (existingItem) { return { cartItems: state.cartItems.map((cartItem) => cartItem.id === item.id ? { ...cartItem, quantity: cartItem.quantity + 1 } : cartItem ), }; } else { return { cartItems: [...state.cartItems, { ...item, quantity: 1 }] }; } }), })); """ ## 4. Performance Optimization These standards focus on optimizing the performance of Zustand-managed applications. ### 4.1. Batching Updates * **Do This:** Use the "batch" function from "react-dom" or "ReactDOM.unstable_batchedUpdates" to batch multiple state updates into a single re-render. **Why:** Batching updates reduces the number of re-renders, improving performance, especially when multiple state changes occur in quick succession. **Example:** """javascript import { unstable_batchedUpdates } from 'react-dom'; import useMyStore from './useMyStore'; function MyComponent() { const { updateValue1, updateValue2 } = useMyStore(); const handleClick = () => { unstable_batchedUpdates(() => { updateValue1('new value 1'); updateValue2('new value 2'); }); }; return <button onClick={handleClick}>Update Values</button>; } """ ### 4.2. Avoiding Unnecessary Re-renders * **Do This:** Use "useMemo" and "useCallback" to memoize expensive calculations and function references. * **Do This:** Ensure components only re-render when their dependencies change. **Why:** Memoization prevents unnecessary re-renders, improving performance, especially for complex components. **Example:** """javascript import { useMemo, useCallback } from 'react'; import useMyStore from './useMyStore'; function MyComponent({ data }) { const { updateValue } = useMyStore(); const expensiveCalculation = useMemo(() => { // Perform a complex calculation based on 'data' return data.reduce((acc, value) => acc + value, 0); }, [data]); const handleClick = useCallback(() => { updateValue(expensiveCalculation); }, [updateValue, expensiveCalculation]); return <button onClick={handleClick}>Update Value</button>; } """ ### 4.3. Debouncing and Throttling * **Do This:** Use debouncing or throttling techniques for actions that are triggered frequently, such as input changes or scroll events. **Why:** Debouncing and throttling limit the number of times an action is executed, preventing performance issues when dealing with high-frequency events. **Example:** """javascript import { debounce } from 'lodash'; import useMyStore from './useMyStore'; function MyComponent() { const { setSearchQuery } = useMyStore(); const debouncedSetSearchQuery = debounce((query) => { setSearchQuery(query); }, 300); const handleInputChange = (event) => { debouncedSetSearchQuery(event.target.value); }; return <input type="text" onChange={handleInputChange} />; } """ ## 5. Testing These conventions apply to testing Zustand stores and components that use them. ### 5.1. Unit Testing * **Do This:** Write unit tests for Zustand stores to verify that state updates and actions behave as expected. * **Do This:** Mock external dependencies and side effects in unit tests. **Why:** Unit tests ensure the correctness and reliability of Zustand stores. **Example:** """javascript import useBearStore from './useBearStore'; import { act } from 'react-dom/test-utils'; describe('useBearStore', () => { it('should initialize with the correct default value', () => { const { result } = renderHook(() => useBearStore()); expect(result.current.bears).toBe(0); }); it('should increase the bear population', () => { const { result } = renderHook(() => useBearStore()); act(() => { result.current.increaseBears(); }); expect(result.current.bears).toBe(1); }); it('should remove all bears', () => { const { result } = renderHook(() => useBearStore()); act(() => { result.current.increaseBears(); result.current.removeAllBears(); }); expect(result.current.bears).toBe(0); }); }); """ ### 5.2. Integration Testing * **Do This:** Write integration tests to verify that components interact correctly with Zustand stores. * **Do This:** Test the behavior of components in different scenarios and with different data. **Why:** Integration tests ensure that components and Zustand stores work together seamlessly. ### 5.3. End-to-End Testing * **Do This:** Use end-to-end (E2E) tests to verify the overall functionality of the application, including Zustand store integration. * **Do This:** Ensure that E2E tests cover critical user flows and interactions. **Why:** E2E tests provide confidence that the application works as expected from the user's perspective. ## 6. Security Best Practices Although Zustand itself isn't inherently a security risk, how you manage the data within your stores and how you interact with external APIs can introduce vulnerabilities. ### 6.1. Data Sanitization * **Do This:** Sanitize any data received from external sources (e.g., user input, API responses) before storing it in Zustand stores. * **Do This:** Follow secure coding practices to prevent cross-site scripting (XSS) and other injection attacks. **Why:** Data sanitization prevents malicious data from being stored and displayed, mitigating security risks. ### 6.2. Authentication and Authorization * **Do This:** Implement proper authentication and authorization mechanisms to protect sensitive data stored in Zustand stores. * **Do This:** Store sensitive information securely (e.g., using encryption). **Why:** Authentication and authorization restrict access to sensitive data, preventing unauthorized access and data breaches. ### 6.3. Secure API Communication * **Do This:** Use HTTPS for all API communication to encrypt data in transit. * **Do This:** Validate API responses to ensure they are well-formed and contain the expected data. **Why:** Secure API communication protects data from eavesdropping and tampering. ### 6.4. Preventing State Tampering * **Do This:** Be aware that client-side state can potentially be tampered with. Don't rely solely on client-side state for critical security decisions. * **Do This:** Perform server-side validation for important operations. **Why:** Client-side state is vulnerable to manipulation, so server-side validation is essential for ensuring data integrity. Don't store sensitive information (like API keys) directly in the Zustand store if it can be avoided. ## 7. Tooling Recommendations * **ESLint:** With plugins for React, TypeScript, and Prettier to enforce code style and best practices. * **Prettier:** To keep consistent code formatting. * **React Developer Tools:** To inspect component state and props, including Zustand store data. * **Testing Libraries:** Jest and React Testing Library provide excellent tooling for unit and integration testing. By adhering to these coding standards and conventions, we can ensure that our Zustand-managed applications are consistent, readable, maintainable, performant, and secure. This leads to higher-quality software and improved team collaboration.
# 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<MyStore>((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 ( <div> <p>API Endpoint: {apiEndpoint}</p> <p>Feature X Enabled: {featureXEnabled ? 'Yes' : 'No'}</p> {/* Component Logic based on featureXEnabled */} </div> ); } 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<MyStoreState>()( 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 ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } 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<MyStoreState>((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<UserInputStoreState>((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.
# 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.
# 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.
# 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.