# Security Best Practices Standards for Zustand
This document outlines security best practices for developing applications using Zustand, a minimalist state management library for React. Adhering to these standards helps protect against common vulnerabilities, ensures data integrity, and maintains a secure application environment.
## 1. General Security Principles
### 1.1. Least Privilege Principle
**Standard:** Grant the minimum necessary access and permissions to your Zustand stores. Avoid storing sensitive data directly in the global store if possible. Access to user-specific data should be controlled and scoped appropriately.
**Why:** Reduces the impact of potential vulnerabilities. If a component or module is compromised, the attacker's access is limited to the data specifically needed by that component, minimizing damage.
**Do This:**
* Structure your stores to separate sensitive and non-sensitive data.
* Use selectors to only expose the data that a component needs.
* Implement role-based access control where applicable.
* For extremely sensitive information, consider not storing it in Zustand at all, and fetching/processing it directly on-demand within components.
**Don't Do This:**
* Store sensitive information (e.g., API keys, passwords, PII) directly in the global Zustand store without proper security measures.
* Grant unnecessarily broad access to the entire store when components only require a small subset of the data.
**Example:**
"""typescript
import { create } from 'zustand';
interface UserState {
id: string;
username: string;
email: string; // Potentially sensitive
role: 'admin' | 'user';
getPublicProfile: () => Partial;
}
const useStore = create((set, get) => ({
id: 'user123',
username: 'john_doe',
email: 'john.doe@example.com', // Potentially sensitive - handle with care
role: 'user',
getPublicProfile: () => {
const { id, username, role } = get(); // Omit the email address
return { id, username, role };
},
}));
export const usePublicProfile = () => useStore((state) => state.getPublicProfile()); // Selector returns limited profile
export const useRole = () => useStore((state) => state.role);
"""
**Explanation:** This example illustrates how to create a store containing a user's complete profile, including potentially sensitive data like email. The "getPublicProfile" selector specifically omits the email, providing a safe way to expose a limited profile. The "useRole" hook demonstrates accessing the role, assumed to be less sensitive.
### 1.2. Data Sanitization and Validation
**Standard:** Sanitize and validate all data received from external sources (e.g., user input, API responses) before storing it in the Zustand store.
**Why:** Prevents injection attacks (e.g., XSS) and ensures data integrity. Storing unsanitized data can lead to unexpected behavior and security breaches.
**Do This:**
* Use input validation libraries (e.g., "yup", "zod") to validate data shapes.
* Sanitize strings to prevent XSS attacks (e.g., using a library like "DOMPurify").
* Validate numerical data to prevent unexpected values that could cause errors.
**Don't Do This:**
* Directly store user input or API responses without validation or sanitization.
* Assume that data from trusted sources is automatically safe.
**Example:**
"""typescript
import { create } from 'zustand';
import * as yup from 'yup';
import DOMPurify from 'dompurify';
interface InputState {
userInput: string;
setUserInput: (input: string) => void;
}
const inputSchema = yup.string().max(255).required();
const useInputStore = create((set) => ({
userInput: '',
setUserInput: (input: string) => {
try {
inputSchema.validateSync(input); // Validate the input
// Sanitize the input for XSS prevention
const sanitizedInput = DOMPurify.sanitize(input);
set({ userInput: sanitizedInput });
} catch (error) {
console.error('Validation Error:', error);
// Handle the error (e.g., display an error message to the user)
}
},
}));
export const useUserInput = () => useInputStore((state) => state.userInput);
export const useSetUserInput = () => useInputStore((state) => state.setUserInput);
"""
**Explanation:** This example demonstrates validating user input against a schema using "yup". The input is also sanitized using "DOMPurify" to prevent Cross-Site Scripting (XSS) attacks. The "try...catch" block handles validation errors gracefully, preventing the store from being updated with invalid data.
### 1.3. Secure Data Handling
**Standard:** Implement encryption for sensitive data stored in the store, especially if the data persists to local storage.
**Why:** Protects sensitive data from unauthorized access if the application is compromised or if the local storage is accessed directly.
**Do This:**
* Use encryption libraries (e.g., "crypto-js", "sjcl") to encrypt sensitive data before storing it.
* Store encryption keys securely, ideally not directly in the client-side code. Explore secure storage options such as hardware security modules (HSMs) or key management services (KMS) if the data sensitivity warrants it.
* Consider using environment variables for encryption keys and manage them securely.
**Don't Do This:**
* Store sensitive data in plain text.
* Hardcode encryption keys in your code.
* Use weak or outdated encryption algorithms.
**Example:**
"""typescript
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import CryptoJS from 'crypto-js';
interface AuthState {
accessToken: string | null;
setAccessToken: (token: string | null) => void;
}
const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY || 'secret-key'; // Store securely!
const useAuthStore = create()(
persist(
(set) => ({
accessToken: null,
setAccessToken: (token: string | null) => {
if (token) {
const encryptedToken = CryptoJS.AES.encrypt(token, ENCRYPTION_KEY).toString();
set({ accessToken: encryptedToken });
} else {
set({ accessToken: null });
}
},
}),
{
name: 'auth-storage', // unique name
storage: localStorage,
serialize: (state) => {
if (state.state.accessToken) {
const encryptedToken = state.state.accessToken
return JSON.stringify({...state, state: {...state.state, accessToken: encryptedToken}})
}
return JSON.stringify(state);
},
deserialize: (str) => {
const parsed = JSON.parse(str);
if (parsed.state.accessToken) {
try {
const bytes = CryptoJS.AES.decrypt(parsed.state.accessToken, ENCRYPTION_KEY);
const decryptedToken = bytes.toString(CryptoJS.enc.Utf8);
return {...parsed, state: {...parsed.state, accessToken: decryptedToken }}
} catch (e) {
console.error("Error decrypting token", e);
return {...parsed, state: {...parsed.state, accessToken: null }} // Handle decryption errors
}
}
return parsed;
},
}
)
);
export const useAccessToken = () => useAuthStore((state) => state.accessToken);
export const useSetAccessToken = () => useAuthStore((state) => state.setAccessToken);
"""
**Explanation:** This example demonstrates encryption of an access token before persisting it to local storage. The "persist" middleware is used to handle storage. The "serialize" function encrypts the token before saving, and the "deserialize" function decrypts it when loading from storage. *Critical*: The "ENCRYPTION_KEY" should *never* be hardcoded directly in your code, as it is here for demonstration purposes. Instead manage it via env variables and secure storage (secrets managers etc). Error handling during token decryption is also included, which gracefully clears the value on a failed decryption.
### 1.4. Preventing State Injection/Manipulation
**Standard:** Be aware of how your store is exposed and prevent unauthorized modification of the state.
**Why:** To avoid potential vulnerabilities where malicious scripts or browser extensions could manipulate the Zustand store leading to unpredictable behavior or security breaches.
**Do This:**
* Ensure that only authorized components or modules can directly modify the state.
* Use immer integration with Zustand for immutable updates to mitigate mutation-based vulnerabilities.
* When passing state or setters to child components, consider using "useCallback" to memoize the setters and prevent unintended re-renders or modifications.
**Don't Do This:**
* Expose the entire "set" function of the store directly to untrusted components.
* Rely solely on client-side validation for critical state changes.
**Example:**
"""typescript
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { useCallback } from 'react';
interface ProfileState {
name: string;
age: number;
updateName: (newName: string) => void;
incrementAge : () => void
}
const useProfileStore = create()(immer((set) => ({
name: 'Initial Name',
age: 30,
updateName: (newName: string) => set((state) => { state.name = newName }),
incrementAge: () => set((state => {state.age += 1}))
})));
export const useProfileName = () => useProfileStore(state => state.name);
export const useProfileAge = () => useProfileStore(state => state.age)
export const useUpdateName = () => useProfileStore(state => state.updateName);
export const useIncrementAge = () => useProfileStore(state => state.incrementAge);
function MyComponent() {
const updateName = useUpdateName();
const incrementAge = useIncrementAge();
// Memoized callbacks
const handleNameChange = useCallback((newName: string) => {
updateName(newName);
}, [updateName]);
const handleAgeIncrement = useCallback(() => {
incrementAge()
}, [incrementAge])
return (
handleNameChange('Updated Name')}>Update Name
handleAgeIncrement()}>Increment Age
);
}
"""
**Explanation:** This example demonstrates using "immer" for immutable updates. This prevents accidental direct mutations of the state. The "useCallback" hook then memoizes the "updateName" callback.
### 1.5. Server-Side Validation
**Standard:** Always perform server-side validation of data before persisting it to the database or using it for critical operations.
**Why:** Client-side validation can be bypassed, making server-side validation essential for data integrity and security.
**Do This:**
* Implement robust server-side validation logic.
* Use prepared statements or parameterized queries to prevent SQL injection.
* Log all failed validation attempts for auditing and security monitoring.
**Don't Do This:**
* Rely solely on client-side validation.
* Trust data received from the client without verification.
**Example:**
(This example shows the *concept*. Zustand runs on the client, so you still require the client to pass the validated/sanitzed data from the server.)
"""typescript
// Client-side (React component using Zustand)
import { create } from 'zustand';
interface FormDataState {
name: string;
email: string;
setName: (name: string) => void;
setEmail: (email: string) => void;
submitForm: () => Promise;
}
const useFormStore = create((set) => ({
name: '',
email: '',
setName: (name) => set({ name }),
setEmail: (email) => set({ email }),
submitForm: async () => {
const { name, email } = useFormStore.getState();
// Client-side validation (for better UX, but NOT sufficient)
if (!name || !email) {
alert('Please fill in all fields.');
return;
}
try {
const response = await fetch('/api/submit', { // Replace with your API endpoint
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email }),
});
if (response.ok) {
alert('Form submitted successfully!');
} else {
const errorData = await response.json();
alert("Form submission failed: ${errorData.message || 'Unknown error'}");
}
} catch (error) {
console.error('Error submitting form:', error);
alert('An error occurred while submitting the form.');
}
},
}));
export const useName = () => useFormStore(state => state.name);
export const useEmail = () => useFormStore(state => state.email);
export const useSetName = () => useFormStore(state => state.setName);
export const useSetEmail = () => useFormStore(state => state.setEmail);
export const useSubmitForm = () => useFormStore(state => state.submitForm);
// Server-side (Next.js API route example)
// /pages/api/submit.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
try {
const validatedData = schema.parse(req.body);
// Process the validated data (save to database, etc.)
console.log('Successfully Validated', validatedData);
res.status(200).json({ message: 'Form submitted successfully!' });
} catch (error) {
if (error instanceof z.ZodError) {
// Return validation errors to the client
return res.status(400).json({ message: 'Validation error', errors: error.errors });
}
console.error('Server Error:', error);
res.status(500).json({ message: 'Internal server error' });
}
} else {
res.status(405).json({ message: 'Method Not Allowed' });
}
}
"""
**Explanation:** The client-side code uses Zustand to manage form data and submit it to a server endpoint. Client-side validation is present for UX. The server-side code (in this case, a Next.js API route) receives the data and validates it using Zod. If the validation errors are handled and sent to the user.
## 2. Specific Zustand Security Considerations
### 2.1. Avoiding Over-Exposure of State
**Standard:** Be mindful of what parts of your state you expose through selectors. Only expose what is necessary for a component.
**Why:** Reduces the risk of components accidentally modifying parts of the store they shouldn't. It also simplifies debugging and maintenance by limiting the scope of each component's interaction with the store.
**Do This:**
* Create specific selectors for each component's needs. Avoid a single "selectAll" selector.
* Consider using a naming convention to clearly indicate which selectors expose sensitive data.
**Don't Do This:**
* Expose entire state slices unnecessarily.
* Create selectors that return mutable objects directly from the store.
**Example:**
"""typescript
import { create } from 'zustand';
interface SettingsState {
theme: 'light' | 'dark';
language: string;
apiKey: string; // Sensitive
setTheme: (theme: 'light' | 'dark') => void;
setLanguage: (language: string) => void;
}
const useSettingsStore = create((set) => ({
theme: 'light',
language: 'en',
apiKey: 'YOUR_API_KEY', // Placeholder - manage securely
setTheme: (theme) => set({ theme }),
setLanguage: (language) => set({ language }),
}));
export const useTheme = () => useSettingsStore((state) => state.theme);
export const useLanguage = () => useSettingsStore((state) => state.language);
// Do NOT expose the apiKey directly! If needed, handle very carefully.
// export const useApiKey = () => useSettingsStore(state => state.apiKey); // AVOID THIS
"""
**Explanation:** This example shows how to expose only the "theme" and "language" settings, while intentionally avoiding direct exposure of the sensitive "apiKey". If "apiKey" access is unavoidable, it should be handled with role-based access control or other security measures.
### 2.2. Secure Store Persistence
**Standard:** When using "zustand/middleware/persist", carefully consider the security implications of storing data in "localStorage" or "sessionStorage".
**Why:** "localStorage" and "sessionStorage" are accessible to JavaScript code within the same origin. If your application is vulnerable to XSS, an attacker could potentially steal data stored in these locations.
**Do This:**
* Encrypt sensitive data before persisting it (as shown in previous examples).
* Consider alternatives to "localStorage" for sensitive data persistence, such as "IndexedDB" with proper security configurations or server-side session management. Be aware that even "IndexedDB" itself can be vulnerable to certain attacks if not handled carefully.
* Implement access controls to limit which parts of the application can read or write data to persisted stores.
**Don't Do This:**
* Store unencrypted sensitive information directly in "localStorage" or "sessionStorage".
* Assume that "localStorage" is a secure storage location.
### 2.3. Handling Asynchronous Actions Safely
**Standard:** When performing asynchronous actions that update the Zustand store, handle errors gracefully and avoid potential race conditions.
**Why:** Unhandled errors in asynchronous actions can lead to inconsistent state and potential vulnerabilities. Race conditions can result in unexpected behavior and data corruption.
**Do This:**
* Use "try...catch" blocks to handle errors in asynchronous functions that update the store.
* Implement loading states to indicate when data is being fetched or processed.
* Use techniques like debouncing or throttling to limit the frequency of asynchronous actions.
**Don't Do This:**
* Ignore errors in asynchronous actions.
* Update the store without handling potential errors from API calls or other asynchronous operations.
**Example:**
"""typescript
import { create } from 'zustand';
interface DataState {
data: any;
loading: boolean;
error: string | null;
fetchData: () => Promise;
}
const useDataStore = create((set) => ({
data: null,
loading: false,
error: null,
fetchData: async () => {
set({ loading: true, error: null }); // Indicate loading state
try {
const response = await fetch('/api/data'); // Replace with your API endpoint
if (!response.ok) {
throw new Error("HTTP error! status: ${response.status}");
}
const data = await response.json();
set({ data, loading: false });
} catch (error: any) {
console.error('Error fetching data:', error);
set({ error: error.message || 'An error occurred', loading: false }); // Handle the error
}
},
}));
export const useData = () => useDataStore(state => state.data);
export const useLoading = () => useDataStore(state => state.loading);
export const useError = () => useDataStore(state => state.error);
export const useFetchData = () => useDataStore(state => state.fetchData);
"""
**Explanation:** This example demonstrates handling errors when fetching data asynchronously. The "loading" state indicates when data is being fetched, and the "error" state stores any errors that occur. The "try...catch" block ensures that errors are caught and handled gracefully, updating the store with the error message.
## 3. Regular Security Audits and Updates
### 3.1. Dependency Management
**Standard:** Keep Zustand and all its dependencies updated to the latest versions.
**Why:** Newer versions often contain security patches that fix known vulnerabilities. Outdated dependencies can introduce security risks.
**Do This:**
* Regularly run "npm update" or "yarn upgrade" to update dependencies.
* Use tools like "npm audit" or "yarn audit" to identify and fix known vulnerabilities in your dependencies.
* Monitor security advisories for Zustand and its dependencies.
**Don't Do This:**
* Ignore dependency updates for extended periods.
* Disable security audits.
### 3.2. Code Reviews
**Standard:** Conduct regular code reviews to identify potential security vulnerabilities.
**Why:** Code reviews provide a fresh perspective and can help catch vulnerabilities that might be missed during development.
**Do This:**
* Have experienced developers review your Zustand code.
* Use automated code analysis tools to identify potential security flaws.
* Document all identified vulnerabilities and the steps taken to address them.
**Don't Do This:**
* Skip code reviews.
* Ignore warnings from code analysis tools.
### 3.3. Penetration Testing
**Standard:** Consider conducting penetration testing to identify vulnerabilities in your application.
**Why:** Penetration testing simulates real-world attacks and can help uncover vulnerabilities that might not be found through other methods.
**Do This:**
* Hire a qualified security professional to conduct penetration testing.
* Address all identified vulnerabilities promptly.
* Retest after fixing vulnerabilities to ensure that they have been properly addressed.
**Don't Do This:**
* Avoid penetration testing.
* Ignore vulnerabilities identified during penetration testing.
By adhering to these coding security standards, you can significantly enhance the security of your Zustand-based applications and protect sensitive data from unauthorized access. This is not an exhaustive list, and security practices should evolve along with new threats and technologies.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for Zustand This document outlines the core architecture standards for Zustand. It is designed to guide developers in creating maintainable, performant, and scalable applications using Zustand as the state management solution. It focuses on fundamental architectural patterns, project structure, and organization principles specifically applied to Zustand within the context of modern JavaScript and React development. ## 1. Project Structure and Organization A well-organized project structure is crucial for maintainability and scalability. When using Zustand, carefully consider how you structure your files, especially as the application grows. ### 1.1 Standard: Feature-Based Organization **Do This:** Organize your project based on features or modules, rather than technology (e.g., components, actions, reducers). Each feature should have its own directory containing all related files, including Zustand stores, components, hooks, and tests. **Don't Do This:** Avoid a flat project structure or organizing files solely by technology type (e.g., all stores in a "/stores" directory, all components in a "/components" directory). **Why:** Feature-based organization improves code discoverability, reduces dependencies between features, and simplifies refactoring. **Example:** """ src/ ├── features/ │ ├── auth/ │ │ ├── authStore.js │ │ ├── components/ │ │ │ ├── Login.jsx │ │ │ └── Logout.jsx │ │ ├── hooks/ │ │ │ └── useAuth.js │ │ └── tests/ │ │ └── authStore.test.js │ ├── dashboard/ │ │ ├── dashboardStore.js │ │ ├── components/ │ │ │ └── Dashboard.jsx │ │ └── ... │ └── ... ├── App.jsx └── ... """ ### 1.2 Standard: Single Store per Feature (Generally) **Do This:** Aim for one primary store per feature. This helps encapsulate the state logic specific to that feature. **Don't Do This:** Creating a monolithic store that manages the entire application state, or excessively fragmenting the state into numerous, overly specific stores, particularly if they have tightly coupled dependencies. **Why:** A single store per feature promotes modularity and reduces the risk of naming conflicts and unintended side effects. Overly granular stores can lead to prop drilling and increased complexity in state synchronization. **Caveat:** There might be cases where multiple stores within a feature are justified (e.g., complex forms with isolated sections, truly independent sub-features). Use your judgment. **Example:** """javascript // src/features/auth/authStore.js import { create } from 'zustand'; const useAuthStore = create((set) => ({ user: null, isAuthenticated: false, login: async (credentials) => { // ... login logic set({ user: { name: 'John Doe' }, isAuthenticated: true }); }, logout: () => set({ user: null, isAuthenticated: false }), })); export default useAuthStore; // src/features/dashboard/dashboardStore.js import { create } from 'zustand'; const useDashboardStore = create((set) => ({ data: [], isLoading: false, fetchData: async () => { set({ isLoading: true }); // ... fetch data logic set({ data: [{ id: 1, name: 'Item 1' }], isLoading: false }); }, })); export default useDashboardStore; """ ### 1.3 Standard: Store Location and Access **Do This:** Place each store file within its respective feature directory. Access stores using custom hooks within the feature's components. **Don't Do This:** Import stores directly into components outside the feature. Avoid placing all stores in a generic "/stores" directory, breaking feature encapsulation. **Why:** This pattern reinforces modularity, making it clear which components depend on which stores and simplifying refactoring. **Example:** """javascript // src/features/auth/hooks/useAuth.js import useAuthStore from '../authStore'; const useAuth = () => { const { user, isAuthenticated, login, logout } = useAuthStore(); return { user, isAuthenticated, login, logout }; }; export default useAuth; // src/features/auth/components/Login.jsx import useAuth from '../hooks/useAuth'; const Login = () => { const { login } = useAuth(); const handleSubmit = (e) => { e.preventDefault(); // ... get credentials from form login({ username: 'test', password: 'password' }); }; return ( <form onSubmit={handleSubmit}> {/* ... form elements */} </form> ); }; export default Login; """ ### 1.4 Standard: Clear Naming Conventions **Do This:** Use consistent and descriptive names for stores and selectors. Prefix store hooks with "use" (e.g., "useAuthStore", "useDashboardStore"). Name selectors based on the data they retrieve (e.g., "selectUserName", "selectIsLoading"). **Don't Do This:** Using generic or ambiguous names that don't clearly indicate the store's purpose or the selector's return value (e.g., "dataStore", "getData"). **Why:** Clear naming improves code readability and makes it easier to understand the purpose of each store and selector. **Example:** """javascript // Good naming const useProductsStore = create((set) => ({ products: [], isLoading: false, fetchProducts: async () => { set({ isLoading: true }); // ... fetch products set({ products: /* fetched products */, isLoading: false }); }, selectProducts: (state) => state.products, selectIsLoading: (state) => state.isLoading, })); // Bad naming const useStore = create((set) => ({ data: [], loading: false, fetchData: async () => { set({ loading: true }); // ... fetch data set({ data: /* fetched data */, loading: false }); }, getData: (state) => state.data, getLoading: (state) => state.loading, })); """ ## 2. Zustand Specific Architecture These standards focus on using Zustand features in an architectural way that promotes maintainability, performance and scalability. ### 2.1 Standard: Immutability **Do This:** Treat state as immutable. Always update state with non-mutating methods. This means using the spread operator ("..."), "Object.assign()", array methods that return new arrays (".map()", ".filter()", ".slice()"), and immutable data structures if needed for performance-critical sections. This is especially critical given potential performance consequences of bypassing shallow equality checks. **Don't Do This:** Mutate state directly (e.g., "state.items.push(newItem)"). This will lead to unexpected behavior and prevent React from efficiently re-rendering components. **Why:** Immutability ensures predictable state updates, simplifies debugging, and enables efficient React re-renders by allowing shallow comparisons to detect changes. It's also crucial when using middleware like "immer" (described later). **Example:** """javascript import { create } from 'zustand'; const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), // Correct: Using spread operator removeItem: (itemId) => set((state) => ({ items: state.items.filter((item) => item.id !== itemId), // Correct: Using filter })), clearCart: () => set({ items: [] }), // Correct: Setting to a new empty array })); export default useCartStore; // Anti-pattern (MUTATING STATE): // addItem: (item) => set((state) => { state.items.push(item); return { items: state.items }; }), """ ### 2.2 Standard: Selectors for Performance Optimization **Do This:** Use selectors to derive specific data from the store. Selectors help components subscribe only to the parts of the state they need, preventing unnecessary re-renders. Use "React.memo" or similar techniques in conjunction with selectors for optimal performance of subscribed components. Leverage the referential equality of selector results to signal component updates. **Don't Do This:** Accessing the entire store in a component and then manually filtering or transforming the data. This forces the component to re-render whenever *any* part of the store changes. Also, avoid creating selectors inside the component, as they'll be re-created on every render, breaking memoization. **Why:** Selectors improve performance by minimizing unnecessary re-renders. **Example:** """javascript import { create } from 'zustand'; const useProductsStore = create((set) => ({ products: [ { id: 1, name: 'Product A', price: 20 }, { id: 2, name: 'Product B', price: 30 }, { id: 3, name: 'Product C', price: 40 }, ], // Selector: Returns only the product names selectProductNames: (state) => state.products.map((product) => product.name), // Selector: Returns a product by ID selectProductById: (state, productId) => state.products.find((product) => product.id === productId), })); export default useProductsStore; // Component using selectors import useProductsStore from './productStore'; import React from 'react'; const ProductList = React.memo(() => { const productNames = useProductsStore((state) => state.selectProductNames); console.log('ProductList re-rendered'); // Only re-renders when product names change return ( <ul> {productNames.map((name) => ( <li key={name}>{name}</li> ))} </ul> ); }); export default ProductList; const ProductDetails = ({ productId }) => { const product = useProductsStore( React.useCallback((state) => state.selectProductById(state, productId), [productId]) ); if (!product) { return <p>Product not found.</p>; } return ( <div> <h3>{product.name}</h3> <p>Price: ${product.price}</p> </div> ); }; """ The "React.memo" in "ProductList" combined with selector ensures the component only rerenders when the array of "productNames" changes (shallow equality comparison). The "ProductDetails" example leverages "useCallback" to memorize and avoid recreating the selector on every render. This avoids unnecessary re-renders and maintains consistent behaviour. ### 2.3 Standard: Middleware Usage and Composition **Do This:** Utilize Zustand's middleware capabilities for cross-cutting concerns like persistence, logging, and immer-based state mutations. Compose middleware carefully, considering their order of execution. Consider splitting complex middleware logic into smaller, reusable functions. **Don't Do This:** Overusing global middleware for feature-specific logic. Ignoring the execution order of middleware, as this can lead to unexpected behavior. **Why:** Middleware promotes separation of concerns, making stores cleaner and easier to test. **Examples:** """javascript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; // Logger middleware (example) const logger = (config) => (set, get, api) => config( (args) => { console.log(' applying', args); set(args); console.log(' new state', get()); }, get, api ); const useStore = create( logger( persist( immer((set) => ({ count: 0, increment: () => set((state) => { state.count += 1 }), // Immer-based mutation decrement: () => set((state) => { state.count -= 1 }), // Immer-based mutation })), { name: 'my-app-storage', storage: createJSONStorage(() => localStorage), } ) ) ); export default useStore; """ **Explanation:** * **"immer":** Allows you to write direct mutations to the state within the "set" function, while Immer handles the immutability under the hood, greatly simplifying complex updates. * **"persist":** Persists the store's state to a storage medium (in this case, "localStorage"). Make sure to use a "createJSONStorage" adapter for handling serialization. Configuration options allow specifying storage name, versioning, and custom (de)serialization. * **"logger":** Logs the state and modifications to the console. This aids in debugging. * **Order of Middleware:** "logger" wraps "persist", which wraps "immer". This means logging will include persisted data. ### 2.4 Standard: Asynchronous Actions and Side Effects **Do This:** Handle asynchronous operations and side effects within store actions. Use "async/await" syntax for cleaner asynchronous code. Consider using a separate utility function (outside the store) to manage the complexities of API requests. **Don't Do This:** Performing side effects directly within components or outside store actions. Directly mutating the store state within asynchronous callbacks. **Why:** Centralizing side effects within actions improves testability, maintainability, and predictability. **Example:** """javascript import { create } from 'zustand'; // Utility function for API requests (outside the store) const fetchProductsFromApi = async () => { const response = await fetch('/api/products'); if (!response.ok) { throw new Error('Failed to fetch products'); } return await response.json(); }; const useProductsStore = create((set) => ({ products: [], isLoading: false, error: null, fetchProducts: async () => { try { set({ isLoading: true, error: null }); const products = await fetchProductsFromApi(); // Use utility function set({ products: products, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useProductsStore; """ ### 2.5 Standard: Deriving State with "get" **Do This**: Inside your store, when a state update depends on the *previous* state *and* other values already in the state, use the "get" function provided by Zustand. This allows you to access the current state within the "set" callback. **Don't Do This**: Trying to access directly the state within the "set" callback without using the "get" function. This may lead to stale values and incorrect state updates. **Why**: "get" ensures you are working with the most up-to-date state within your store. This is particularly important and prevents race-conditions with async operations. **Example**: """javascript import { create } from 'zustand'; const useCounterStore = create((set, get) => ({ count: 0, step: 1, // Increment step increment: () => set(state => ({ count: state.count + get().step })), // Increment using the step setStep: (step) => set({ step: step }), // Updates the step })); export default useCounterStore; """ ### 2.6 Standard: Context Usage (Minimal) **Do This:** Prefer direct store usage with the "useStore" hook within components. Only use React Context if absolutely necessary, typically for providing initial state or global configurations to Zustand stores—and preferably using the "context" option in "create" hook. **Don't Do This:** Overuse React Context for passing frequently updated state, as this can negate the benefits of Zustand's selective re-rendering. **Why:** Zustand is designed to be used independently of React Context for most state management needs. Over-reliance on Context can hurt performance. **Example (minimal Context Usage):** """javascript // Store with initial value coming from a Context import { createContext, useContext } from 'react'; import { create } from 'zustand'; const ConfigContext = createContext({ apiEndpoint: '/default/api' }); const useProductsStore = create((set, get, api) => ({ ... }), {context: ConfigContext}); export default useProductsStore; const MyComponent = () => { const products = useProductsStore(state => state.products); return ( {/* ... */} ) } """ ### 2.7 Standard: Data Fetching Best Practices **Do This:** Separate data fetching logic from your Zustand store. Use custom hooks to encapsulate data fetching and update the store accordingly. **Don't Do This:** Directly performing data fetching inside components. This can lead to tight coupling and make testing difficult. **Why:** This improves code organization, testability, and reusability. **Example:** """javascript // Custom hook for fetching data import { useState, useEffect } from 'react'; import useProductsStore from './productStore'; const useFetchProducts = () => { const { fetchProducts } = useProductsStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { setIsLoading(true); setError(null); try { await fetchProducts(); } catch (err) { setError(err); } finally { setIsLoading(false); } }; fetchData(); }, [fetchProducts]); return { isLoading, error }; }; export default useFetchProducts; // Component using the custom hook import useFetchProducts from './useFetchProducts'; import useProductsStore from './productStore'; const ProductList = () => { const { isLoading, error } = useFetchProducts(); const products = useProductsStore((state) => state.products); if (isLoading) { return <p>Loading products...</p>; } if (error) { return <p>Error: {error.message}</p>; } return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ); }; export default ProductList; """ These standards, when applied consistently, will result in a more robust, maintainable, and performant Zustand-based application. Remember to prioritize clarity and modularity in your architecture, and choose the right tool for the job (selector or whole state).
# Component Design Standards for Zustand This document outlines the coding standards for component design when using Zustand for state management in React applications. These standards promote reusability, maintainability, and performance, and are tailored for the latest Zustand features and best practices. ## 1. General Principles ### 1.1. Single Responsibility Principle (SRP) * **Do This:** Design components so each has only one reason to change. For example, a component rendering user data shouldn't also handle authentication. Separate concerns into distinct components (some of which internally use zustand stores). * **Don't Do This:** Create monolithic components handling multiple unrelated functionalities. This makes debugging and maintenance difficult. * **Why:** SRP leads to more modular, testable, and understandable code. Changes in one area are less likely to impact unrelated parts of the application. * **Example:** """jsx // Good: Separate components for user data and profile update const UserData = () => { const userData = useStore(state => state.user); return ( <div> <h1>{userData.name}</h1> <p>{userData.email}</p> </div> ); }; const UpdateProfile = () => { const updateUser = useStore(state => state.updateUser); const [name, setName] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); updateUser({name}); } return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <button type="submit">Update Name</button> </form> ); }; """ """jsx // Bad: A single component doing too much const UserProfile = () => { const userData = useStore(state => state.user); const updateUser = useStore(state => state.updateUser); const [name, setName] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); //Also handles OTHER unrelated operations here. BAD. updateUser({name}); } return ( <div> <h1>{userData.name}</h1> <p>{userData.email}</p> <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} /> <button type="submit">Update Name</button> </form> </div> ); }; """ ### 1.2. Separation of Concerns (SoC) * **Do This:** Isolate distinct parts of your application (UI, state management, data fetching) into separate modules. Use custom hooks to abstract Zustand logic from components, promoting better SoC - especially for reading data. For mutations write discrete reusable setters in the zustand store. * **Don't Do This:** Mix UI rendering logic directly with state management or data fetching within a single component. * **Why:** SoC improves maintainability, testability, and reusability. It also makes it easier to understand and modify specific parts of the application. * **Example:** """jsx // Good: Custom hook for fetching and exposing user data import create from 'zustand'; // Ensure you import create from zustand const useUserStore = create((set) => ({ user: null, fetchUser: async (id) => { const response = await fetch("/api/users/${id}"); const data = await response.json(); set({ user: data }); }, updateUser: (newUserData) => set(state => ({ ...state, user: { ...state.user, ...newUserData } })), })); // User component consuming the hook const UserProfile = () => { const user = useUserStore(state => state.user); const fetchUser = useUserStore(state => state.fetchUser); React.useEffect(() => { fetchUser(123); // Example user ID }, [fetchUser]); if (!user) { return <p>Loading user data...</p>; } return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }; """ """jsx // Bad: Mixing data fetching and UI rendering const UserProfileBad = () => { const [user, setUser] = React.useState(null); React.useEffect(() => { const fetchUser = async (id) => { const response = await fetch("/api/users/${id}"); const data = await response.json(); setUser(data); }; fetchUser(123); }, []); if (!user) { return <p>Loading user data...</p>; } return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> ); }; """ ### 1.3. Component Composition * **Do This:** Favor composition over inheritance. Create smaller, focused components and combine them to build more complex UIs. * **Don't Do This:** Create deep inheritance hierarchies, leading to inflexible and hard-to-understand code. * **Why:** Composition provides more flexibility and reusability. It allows you to easily combine and rearrange components to create diverse UIs. * **Example:** """jsx // Good: Composing smaller components const Button = ({ children, onClick }) => ( <button onClick={onClick}>{children}</button> ); const Input = ({ type, value, onChange }) => ( <input type={type} value={value} onChange={onChange} /> ); const Form = () => { const [email, setEmail] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); alert("Email submitted: ${email}"); }; return ( <form onSubmit={handleSubmit}> <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /> <Button type="submit">Submit</Button> </form> ); }; """ ## 2. Zustand-Specific Component Design ### 2.1. Selective Subscriptions with "useStore" * **Do This:** Use the selector function in "useStore" to subscribe only to the parts of the store that a component actually needs. This minimizes re-renders, boosting performance. Leverage "useShallow" and "equalityFn" options when appropriate. * **Don't Do This:** Subscribe to the entire store if the component only needs a small piece of data. This leads to unnecessary re-renders. * **Why:** Selective subscriptions prevent components from re-rendering when unrelated parts of the store change. This significantly improves performance, especially in complex applications. * **Example:** """jsx // Good: Selecting only the "todos" array import create from 'zustand'; import { shallow } from 'zustand/shallow'; const useTodoStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) })), filter: 'all', setFilter: (filter) => set({ filter }), })); const TodoList = () => { //Selects only the todos and toggleTodo from the zustand store. const [todos, toggleTodo] = useTodoStore(state => [state.todos, state.toggleTodo], shallow); return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} </li> ))} </ul> ); }; """ """jsx // Bad: Subscribing to the entire store (BAD PRACTICE) const TodoListBad = () => { const { todos, toggleTodo } = useTodoStore(); // Gets EVERYTHING return ( <ul> {todos.map(todo => ( <li key={todo.id}> <input type="checkbox" checked={todo.completed} onChange={() => toggleTodo(todo.id)} /> {todo.text} </li> ))} </ul> ); }; """ ### 2.2. Derived State with Selectors * **Do This:** Use selectors within "useStore" to derive new state values from the store's state. Avoid performing calculations directly within the component if that derived value is needed in multiple components or could be memoized for performance gains. * **Don't Do This:** Recompute derived state within components on every render, especially if the calculations are expensive. * **Why:** Selectors allow you to efficiently compute derived state and only trigger re-renders when the relevant parts of the store change. * **Example:** """jsx import create from 'zustand'; const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) })), })); const CartTotal = () => { // Selector to calculate the total price const total = useCartStore(state => state.items.reduce((sum, item) => sum + item.price, 0) ); return ( <p>Total: ${total}</p> ); }; """ ### 2.3. Using "useStore.getState" Sparingly * **Do This:** Primarily use "useStore(selector)" for accessing state within components. Reserve "useStore.getState()" for cases where you need to access the store's value outside of the component lifecycle (ex: event handlers, testing, or directly interacting with the store). * **Don't Do This:** Rely heavily on "useStore.getState()" inside React component render functions; this bypasses subscriptions and can lead to stale data and performance issues. * **Why:** "useStore(selector)" drives efficient component re-renders and reactivity based on zustand store changes. "useStore.getState()" is a snapshot, and does not cause functional components using reactiveness to re-render.. * **Example:** """jsx import create from 'zustand'; const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); const CounterComponent = () => { const count = useCounterStore((state) => state.count); // Use selector for reactivity const increment = useCounterStore(state => state.increment); //Example of getState usage. const logCount = () => { //This is fine because it's not in the render function directly. const currentCount = useCounterStore.getState().count; console.log('Current count:', currentCount); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={logCount}>Log count</button> </div> ); }; """ ### 2.4 Local vs global state * **Do This:** Use local state for presentational and UI concerns. Use Zustand to reflect your data model. * **Don't Do This:** Put EVERYTHING into Zustand. Components should continue to manage transient UI-related state (form input values, active tab state, etc) without involving Zustand, unless that state needs to be shared or persisted. * **Why:** Over-using Zustand can lead to unnecessary complexity and performance overhead. Local state can be more efficient for component-specific concerns with limited scope. * **Example:** """jsx // Good: Local state for input value, Zustand for user data import create from 'zustand'; const useUserStore = create((set) => ({ user: { name: '', email: '' }, updateUser: (userData) => set({ user: userData }), })); const UserForm = () => { const [name, setName] = React.useState(''); // Local state for input const [email, setEmail] = React.useState(''); // Local state const updateUser = useUserStore(state => state.updateUser); const handleSubmit = (e) => { e.preventDefault(); updateUser({ name, email }); // Update Zustand store }; return ( <form onSubmit={handleSubmit}> <label>Name: <input type="text" value={name} onChange={(e) => setName(e.target.value)} /></label> <label>Email: <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></label> <button type="submit">Update Profile</button> </form> ); }; """ ### 2.5 Managing Asynchronous Actions * **Do This:** Define async actions (like data fetching) within your Zustand store. Use "try...catch" blocks to handle potential errors and update the store's state accordingly (e.g., setting an "error" flag). If possible, isolate your data access logic using custom hooks to help with SoC and reusability. * **Don't Do This:** Perform data fetching directly within your components without using any error handling and proper state management. Avoid mutating store values directly within the components. * **Why:** Keep your code clean, maintainable, and testable. Centralized async actions are easier to manage and provide a single source of truth. * **Example:** """jsx import create from 'zustand'; const useProductsStore = create((set) => ({ products: [], loading: false, error: null, fetchProducts: async () => { set({ loading: true, error: null }); try { const response = await fetch('/api/products'); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } const data = await response.json(); set({ products: data, loading: false }); } catch (e) { set({ error: e.message, loading: false }); } }, })); const ProductsList = () => { const {products, loading, error, fetchProducts} = useProductsStore(); useEffect(() => { fetchProducts(); }, [fetchProducts]); if (loading) return <p>Loading products...</p>; if (error) return <p>Error: {error}</p>; return ( <ul> {products.map(product => ( <li key={product.id}>{product.name} - ${product.price}</li> ))} </ul> ); }; """ ## 3. Advanced Patterns and Considerations ### 3.1. Zustand Middleware * **Do This:** Leverage Zustand's middleware features (e.g., "persist", "devtools", "immer") to add cross-cutting concerns to your state management without cluttering component logic. * **Don't Do This:** Implement persistence, debugging, or immutable updates manually in components. * **Why:** Middleware provides a clean and composable way to enhance Zustand stores with features like persistence, debugging, and immutable updates. * **Example:** """jsx import create from 'zustand'; import { persist } from 'zustand/middleware'; import { devtools } from 'zustand/middleware'; import produce from 'immer'; const useStore = create(devtools(persist( (set) => ({ todos: [], addTodo: (text) => set(produce((state) => { state.todos.push({ id: Date.now(), text, completed: false }); })), }), { name: 'todo-store', // unique name getStorage: () => localStorage, } ))); """ ### 3.2. Testing Components with Zustand * **Do This:** Mock the Zustand store in your component tests to isolate the component and make tests predictable. Consider using a testing library (e.g., Jest, React Testing Library) and mock the Zustand store using "jest.mock". Use "act" when updating the Zustand store within tests. * **Don't Do This:** Directly manipulate the Zustand store in tests without using mocking, leading to unpredictable test results and potential interference between tests. * **Why:** Mocking allows you to control the store's state and actions during testing, ensuring consistent and reliable test results and preventing interference between tests. * **Example:** """jsx // __mocks__/zustand.js const actualZustand = jest.requireActual('zustand'); let storeValues = {}; const mockSet = (fn) => { storeValues = typeof fn === "function" ? fn(storeValues) : fn; }; const useStore = () => ({ getState: () => storeValues, setState: mockSet, subscribe: () => {}, destroy: () => {} }); useStore.setState = mockSet; useStore.getState = () => storeValues; useStore.subscribe = () => {}; useStore.destroy = () => {}; module.exports = actualZustand; module.exports.create = () => useStore module.exports.useStore = useStore // Component.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import TodoList from './TodoList'; import * as zustand from 'zustand'; jest.mock('zustand'); describe('TodoList Component', () => { it('allows adding a todo to the list', () => { const mockStore = { todos: [], addTodo: jest.fn() }; const { useStore } = zustand; useStore.mockReturnValue(mockStore); render(<TodoList />); const inputElement = screen.getByPlaceholderText('Add todo'); const addButtonElement = screen.getByText('Add'); act(() => { fireEvent.change(inputElement, { target: { value: 'Buy milk' } }); fireEvent.click(addButtonElement); }); expect(mockStore.addTodo).toHaveBeenCalledWith('Buy milk'); }); }); """ ### 3.3. Performance Optimization Strategies * **Do This:** Use "React.memo" and "useCallback" in conjunction with Zustand selectors to prevent unnecessary re-renders of components. Make sure your custom hook returns a stable reference of the store or selector. * **Don't Do This:** Rely solely on Zustand to optimize performance, without considering React's own optimization techniques. * **Why:** "React.memo" memoizes components based on their props. Using useCallback on the state management actions passed as props can drastically eliminate rerenders when props are exactly the same. ### 3.4 Type Safety with TypeScript * **Do This:** Use TypeScript to define the shape of your Zustand store's state and actions. This provides compile-time type checking and helps prevent runtime errors. * **Example:** """typescript import create from 'zustand'; interface UserState { id: number; name: string; email: string; } interface UserActions { updateName: (newName: string) => void; updateEmail: (newEmail: string) => void; } type UserStore = UserState & UserActions; const useUserStore = create<UserStore>((set) => ({ id: 1, name: 'John Doe', email: 'john.doe@example.com', updateName: (newName) => set({ name: newName }), updateEmail: (newEmail) => set({ email: newEmail }), })); """ These coding standards for Zustand component design will help you write maintainable, performant, and testable React applications. By following these guidelines, your team can contribute to a codebase that is easy to understand, modify, and extend.
# State Management Standards for Zustand This document provides comprehensive coding standards for state management using Zustand. These standards are designed to ensure maintainable, performant, and secure applications. It is tailored for developers of all experience levels, providing guidance for code clarity and consistency. ## 1. Core Principles of Zustand State Management ### 1.1 Simplicity and Minimalism **Do This:** * Embrace Zustand's simplicity. Keep state definitions concise and business-logic-focused. Avoid over-complicating state with derived data unless absolutely necessary. **Don't Do This:** * Introduce unnecessary abstractions or layers of complexity. Zustands strength lies in direct access and mutations. Don't try to mimic Redux-style architectures. **Why:** Simplicity reduces cognitive load, enables faster development, and makes debugging easier. It also aligns with Zustands core philosophy. **Example:** """javascript import { create } from 'zustand' const useBearStore = create((set) => ({ bears: 0, increaseBears: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) export default useBearStore; """ ### 1.2 Explicit Data Flow **Do This:** * Define clear data flow paths. Actions within the store should be the primary way to update the state. This promotes maintainability and traceability. **Don't Do This:** * Mutate state directly outside of the store. This breaks the unidirectional data flow, which makes debugging and reasoning about your application state difficult. **Why:** Explicit data flow improves predictability and makes it easier to track state changes. **Example:** """javascript import { create } from 'zustand' const useProductsStore = create((set) => ({ products: [], fetchProducts: async () => { const response = await fetch('/api/products'); // Simulating product API const data = await response.json(); set({ products: data }); }, addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductsStore; """ ### 1.3 Reactivity and Selectors **Do This:** * Use selectors to extract specific parts of the state that components need. Subscribing components only to the necessary parts of the state enhances performance. * Memoize selectors to avoid unnecessary re-renders. When using selectors frequently, especially with large state objects, memoization can substantially reduce render load. **Don't Do This:** * Subscribe components to the entire store state when only a small part of it is needed. * Compute values directly within components that are derived from the state. This tightly couples logic to components and makes it hard to optimize re-renders. **Why:** Selectors and memoization improve performance by reducing the amount of re-rendering in your React components. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) })), totalItems: () => { // Derived Value, could be enhanced return useCartStore.getState().items.length; }, totalPrice: () => { // Derived Value, could be enhanced return useCartStore.getState().items.reduce((acc, item) => acc + item.price, 0) } })) // Component Using Selectors, SHALLOW comparison for re-renders! function CartSummary() { const [totalItems, totalPrice] = useCartStore( (state) => [state.totalItems, state.totalPrice], shallow ) return ( <div> Total Items: {totalItems()} Total Price: ${totalPrice()} </div> ) } export default useCartStore; """ ### 1.4 Asynchronous Actions **Do This:** * Handle asynchronous logic (e.g., API fetching) directly inside store actions. Use "async/await" or "Promises" for clarity. **Don't Do This:** * Perform asynchronous actions outside of the store and then update the state. This can lead to race conditions or inconsistent state updates. **Why:** Centralizing asynchronous operations within the store provides better control and error handling. **Example:** """javascript import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, isLoading: false, error: null, fetchUser: async (userId) => { set({ isLoading: true, error: null }); // Set loading state try { const response = await fetch("/api/users/${userId}"); const data = await response.json(); set({ user: data, isLoading: false }); // Update user data and reset loading } catch (error) { set({ error: error.message, isLoading: false }); // Set error state } }, })); export default useUserStore; """ ## 2. Zustand Store Structure and Organization ### 2.1 Modular Stores **Do This:** * Divide your application state into smaller, manageable stores based on logical domains (e.g., user, product, cart). Favor Composition over Inheritance. **Don't Do This:** * Create a single monolithic store for the entire application if your app grows complex. This will lead to performance bottlenecks and make the code hard to follow. **Why:** Modular stores improve code organization, reduce the scope of changes, and make it easier to test and reuse state logic. **Example:** """javascript // userStore.js import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user }), })); export default useUserStore; // productStore.js import { create } from 'zustand' const useProductStore = create((set) => ({ products: [], addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductStore; // cartStore.js import { create } from 'zustand' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })); export default useCartStore; """ ### 2.2 Group Related State **Do This:** * Logically group pieces of the store that are related. Avoid scattering related state properties across the store definition. **Don't Do This:** * Separating the "isLoading" loading flag, and error states from async actions that they're associated with. **Why:** Improve code readability and developer experience. **Example:** """javascript import { create } from 'zustand' const useAuthStore = create((set) => ({ authState: { // Grouping properties isAuthenticated: false, token: null, isLoading: false, error: null }, login: async (username, password) => { set((state) => ({ authState: { ...state.authState, isLoading: true, error: null } })); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const data = await response.json(); set((state) => ({ authState: { isAuthenticated: true, token: data.token, isLoading: false, error: null } })); } catch (error) { set((state) => ({ authState: { ...state.authState, isLoading: false, error: error.message } })); } }, logout: () => { set((state) => ({ authState: { isAuthenticated: false, token: null, isLoading: false, error: null } })); }, })); export default useAuthStore; """ ### 2.3 Using "persist" Middleware **Do This:** * Use "zustand/middleware"'s "persist" middleware when you need to persist state across browser sessions. This is especially useful for user authentication or cart data. Configuring the storage adapter when necessary. **Don't Do This:** * Persist sensitive information directly into local storage without proper encryption. * Persisting state unnecessarily without consideration for the limited local storage budget. **Why:** Persistence enhances user experience by preserving state across sessions but necessitates cautious handling of sensitive data. **Example:** """javascript import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' const useSettingsStore = create( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'settings-storage', // unique name storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used } ) ); export default useSettingsStore; """ ## 3. Zustand Action Design ### 3.1 Atomic Updates **Do This:** * Design actions to perform atomic updates to the state. Each action should represent a single, logical change to the state. **Don't Do This:** * Bunch unrelated state updates in a single action, making maintenance difficult. * Create actions that trigger cascading updates across multiple stores. This tightens coupling. **Why:** Atomic updates lead to simpler and more predictable state transitions, which are easier to test and debug. **Example:** """javascript import { create } from 'zustand' const useTaskStore = create((set) => ({ tasks: [], addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })), removeTask: (taskId) => set((state) => ({ tasks: state.tasks.filter((task) => task.id !== taskId) })), updateTaskStatus: (taskId, completed) => // Focused Update set((state) => ({ tasks: state.tasks.map((task) => task.id === taskId ? { ...task, completed } : task ), })), })); export default useTaskStore; """ ### 3.2 Naming Conventions **Do This:** * Use clear and consistent naming conventions for actions. Action names should describe what they do. A good naming system is "verbNoun": e.g. "addProduct", "removeUser" etc. **Don't Do This:** * Use vague or ambiguous action names that don't clearly indicate their purpose. **Why:** Consistent naming ensures code readability and makes it easier to understand the purpose of each action. **Example:** """javascript import { create } from 'zustand' const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); export default useCounterStore; """ ### 3.3 Decoupled Logic **Do This:** * Keep store logic focused on state updates. Delegate complex business logic to separate utility functions or services. **Don't Do This:** * Embed complex computations or algorithms directly within the state update functions. **Why:** Decoupled logic makes stores easier to understand, test, and maintain. **Example:** """javascript // utils/taxCalculator.js export const calculateTax = (price, taxRate) => { return price * taxRate; }; // store.js import { create } from 'zustand' import { calculateTax } from './utils/taxCalculator'; const useProductStore = create((set) => ({ price: 100, taxRate: 0.10, setTaxRate: (rate) => set({ taxRate: rate }), calculateTaxedPrice: () => { // Moved the calculateTax logic to external utility. Very easy to test! return useProductStore.getState().price + calculateTax(useProductStore.getState().price, useProductStore.getState().taxRate); }, })); export default useProductStore; """ ## 4. Optimization and Performance ### 4.1 Shallow Equality **Do This:** * When selecting state, use "shallow" from "zustand/shallow" for shallow equality checks to prevent unnecessary re-renders. * Structure your state to encourage shallow comparisons. Avoid deeply nested objects when possible. **Don't Do This:** * Skip shallow equality checks when performance is important. * Modify nested objects within the state immutably; otherwise, shallow comparisons won't work as expected. **Why:** Shallow equality ensures that components only re-render when the relevant state actually changes, reducing unnecessary updates. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useProfileStore = create((set) => ({ profile: { name: 'John Doe', age: 30 }, updateName: (name) => set((state) => ({ profile: { ...state.profile, name } })), updateAge: (age) => set((state) => ({ profile: { ...state.profile, age } })), })); function ProfileComponent() { const { name, age } = useProfileStore( (state) => ({ name: state.profile.name, age: state.profile.age }), shallow ); return ( <div> Name: {name}, Age: {age} </div> ); } export default useProfileStore; """ ### 4.2 Minimize State Size **Do This:** * Only store essential data in Zustand store. Avoid duplicating data or storing derived data if it can be easily computed. **Don't Do This:** * Inflate stores with large or redundant data, increasing memory usage and potentially affecting performance. **Why:** Minimize state size improves performance by reducing the overhead of state updates and re-renders. ### 4.3 Immutability **Do This:** * Ensure that all operations in the store are done immutably to trigger re-renders. * Use "..." spread operator or lodash/immer to ensure immutability **Don't Do This:** * Directly mutate the state. **Why:** Immutability ensure that new references are created and React knows to re-render the component to reflect the changes in the store. **Example:** """javascript import { create } from 'zustand' const useTodosStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), })), })); export default useTodosStore; """ ## 5. Testing ### 5.1 Unit Testing **Do This:** * Write unit tests for your Zustand stores to ensure that actions update the state correctly. * Test actions with different inputs and check for expected state changes. **Don't Do This:** * Skip testing Zustand stores, assuming they are simple and error-free. **Why:** Unit tests ensure that your state management logic is correct. **Example:** """javascript import { create } from 'zustand' import { act } from 'react-dom/test-utils'; import { renderHook } from '@testing-library/react'; const useTestStore = create((set) => ({ value: 0, increment: () => set((state) => ({ value: state.value + 1 })), setValue: (newValue) => set({ value: newValue }), })); describe('useTestStore', () => { beforeEach(() => { // Reset the store before each test useTestStore.setState({value: 0}, true); }); it('should initialize with a value of 0', () => { const { result } = renderHook(() => useTestStore()); expect(result.current.value).toBe(0); }); it('should increment the value by 1', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.increment(); }); expect(useTestStore.getState().value).toBe(1); }); it('should set the value to a new value', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.setValue(10); }); expect(useTestStore.getState().value).toBe(10); }); }); export default useTestStore; """ ### 5.2 Integration Testing **Do This:** * Test how Zustand stores interact with your React components and other parts of your application. **Don't Do This:** * Ignore integration tests, as they ensure that the entire system works correctly. **Why:** Integration tests verify that your state management solution integrates seamlessly with the rest of your application. ### 5.3 Mocking Dependencies **Do This:** * Mock external dependencies like APIs in your tests to ensure that tests are fast and reliable. **Don't Do This:** * Rely on live APIs in your tests, which can make the tests slow and flaky. **Why:** Mocking dependencies isolates your components and stores, allowing you to test them in a controlled environment. ## 6. Security Considerations ### 6.1 Data Encryption **Do This:** * If you are persisting sensitive data using "zustand/middleware" persisted state, encrypt the data before storing it in local storage. **Don't Do This:** * Store sensitive data in plain text. **Why:** Encryption protects sensitive data from unauthorized access, ensuring a higher level of security for your application. ### 6.2 Input Validation **Do This:** * Validate all inputs to Zustand store actions to prevent invalid data from being stored. **Don't Do This:** * Trust user input without validation. **Why:** Input validation prevents security vulnerabilities like cross-site scripting (XSS) and SQL injection. ### 6.3 Authentication and Authorization **Do This:** * Implement proper authentication and authorization mechanisms to protect sensitive state and actions. **Don't Do This:** * Expose sensitive actions or state to unauthorized users. **Why:** Authentication and authorization ensure that only authorized users can access and modify sensitive data, protecting your application from malicious attacks. ## 7. Conclusion By adhering to these Zustand coding standards, you can create maintainable, performant, and secure state management solutions. Zustand provides a simple and powerful way to manage state in React applications, and these guidelines will help you use it effectively. Remember to prioritize simplicity, clarity, and testability in your code.
# Performance Optimization Standards for Zustand This document outlines best practices for optimizing the performance of Zustand stores in React applications. It aims to provide clear guidelines for developers to write efficient, responsive, and resource-friendly code when using Zustand. ## 1. Selective State Updates ### Standard 1: Avoid Unnecessary Rerenders with Shallow Equality **Do This:** Use "shallow" equality check within "useStore" selector functions to prevent rerenders when the selected state doesn't actually change. **Don't Do This:** Directly destructure state inside "useStore" without a selector or without shallow comparison, as this will cause rerenders on every store update, even if relevant data hasn't changed. **Why This Matters:** React components rerender when their props change. Zustand's "useStore" hook triggers a rerender whenever the store updates unless you use a selector. Without shallow equality, even if the underlying data within the store remains the same, the component will rerender, leading to performance bottlenecks, especially in complex UIs. **Code Example:** """javascript import { create } from 'zustand'; import { shallow } from 'zustand/shallow'; const useStore = create((set) => ({ user: { id: 1, name: 'John Doe', age: 30 }, updateUserAge: (age) => set(state => ({ user: { ...state.user, age } })) })); function UserProfile() { // Correct: Using a selector with shallow equality, avoids rerenders // if only other properties of "user" change const { name, age } = useStore((state) => ({ name: state.user.name, age: state.user.age }), shallow); // Incorrect: Direct destructuring causes rerenders on every store update even if name and age didn't change. // const { user, updateUserAge } = useStore(); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); } export default UserProfile; """ ### Standard 2: Minimize Selector Granularity **Do This:** Select only the specific data a component needs from the Zustand store. Create multiple, focused selectors within a component if necessary. **Don't Do This:** Select large portions of the store and then filter or transform the data within the component. This triggers rerenders even if the component only uses a small subset of the selected data and results in more processing work per render. **Why This Matters:** The goal is to minimize the amount of data that triggers a rerender. Selecting only essential data ensures rerenders are triggered only when absolutely necessary. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ items: [ { id: 1, name: 'Apple', price: 1.0 }, { id: 2, name: 'Banana', price: 0.5 }, { id: 3, name: 'Orange', price: 0.75 }, ], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })); function ItemList() { // Correct: Selects only item names and prices const itemNamesAndPrices = useStore(state => state.items.map(item => ({ name: item.name, price: item.price }))); // Incorrect: Selecting the entire "items" array. If we only need the names and prices, it's inefficient. // const items = useStore(state => state.items); return ( <ul> {itemNamesAndPrices.map(item => ( <li key={item.name}> {item.name} - ${item.price} </li> ))} </ul> ); } export default ItemList; """ ### Standard 3: Memoize Selectors for Complex Transformations **Do This:** Use memoization techniques (e.g., "useMemo" in React, or libraries like "reselect") for selectors that perform expensive calculations or transformations. **Don't Do This:** Perform the same complex calculations within selectors on every render. This adds overhead and slows down the application, especially with large datasets. **Why This Matters:** Memoization caches the results of expensive operations, returning the cached result if the input dependencies haven't changed. This avoids redundant calculations and significantly enhances performance. **Code Example:** """javascript import { create } from 'zustand'; import { useMemo } from 'react'; const useStore = create((set) => ({ products: [ { id: 1, name: 'Laptop', price: 1200, category: 'Electronics' }, { id: 2, name: 'Keyboard', price: 100, category: 'Electronics' }, { id: 3, name: 'T-Shirt', price: 25, category: 'Clothing' }, ], })); function ProductList({ category }) { const products = useStore(state => state.products); // Memoize the filtered products based on the selected category const filteredProducts = useMemo(() => { console.log('Filtering products...'); // This should only log when the 'category' prop changes. return products.filter(product => product.category === category); }, [products, category]); // Depend on 'products' *and* 'category'! return ( <ul> {filteredProducts.map(product => ( <li key={product.id}> {product.name} - ${product.price} </li> ))} </ul> ); } export default ProductList; """ ## 2. Optimizing Store Updates ### Standard 4: Batch Multiple State Updates **Do This:** Use the functional form of "set" to batch multiple related state updates within a single call. **Don't Do This:** Trigger multiple independent calls to "set" for related state updates. Each "set" triggers rerenders, so batching reduces the total number of rerenders improving performance. **Why This Matters:** React can batch updates, reducing the overhead of multiple rerenders. By combining related updates into single "set" operations, you can improve application responsiveness. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ count: 0, name: 'Initial Name', incrementAndRename: () => set(state => ({ count: state.count + 1, name: "Name ${state.count + 1}" })), // Batched into a single update //Don't Do this: increment: () => set(state => ({count: state.count + 1})), rename: (newName) => set({ name: newName}) })); function Counter() { const { count, name, incrementAndRename } = useStore(); return ( <div> <p>Count: {count}</p> <p>Name: {name}</p> <button onClick={incrementAndRename}>Increment and Rename</button> </div> ); } export default Counter; """ ### Standard 5: Debounce or Throttle Frequent Updates **Do This:** Use debouncing or throttling techniques when handling frequent and potentially redundant data updates (e.g., handling input changes, resizing). Libraries like "lodash" provide utilities for this. **Don't Do This:** Directly update the store on every single event trigger if the intermediate values are not important. This can lead to excessive rerenders and performance degradation. **Why This Matters:** Debouncing and throttling limit the rate at which a function is executed. Debouncing ensures a function is only called after a certain amount of time has passed since the last call. Throttling ensures a function is called at most once within a specified period. This is crucial for performance when dealing with high-frequency events. **Code Example:** """javascript import { create } from 'zustand'; import { debounce } from 'lodash'; const useStore = create((set) => ({ searchTerm: '', setSearchTerm: debounce((term) => { console.log("Updating search term to ${term}"); set({ searchTerm: term }); }, 300), // Debounce for 300ms })); function SearchInput() { const { searchTerm, setSearchTerm } = useStore(); const handleChange = (event) => { setSearchTerm(event.target.value); }; return ( <input type="text" value={searchTerm} onChange={handleChange} placeholder="Search..." /> ); } export default SearchInput; """ ### Standard 6: Leverage "useStore.setState"'s Immutability **Do This:** Within "useStore.setState", always return *new* objects and arrays rather than modifying existing ones. **Don't Do This:** Mutate existing arrays or objects directly within the "set" function. This can lead to unexpected behavior and difficulties in tracking state changes. **Why This Matters:** Zustand relies on immutability to determine when state changes occur. Mutating state directly can bypass this mechanism, causing components not to rerender correctly or lead to subtle bugs. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ items: [{ id: 1, name: 'Item 1' }], addItem: (newItem) => set((state) => ({ items: [...state.items, newItem], // Correct: Creates a new array with added item. })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) //Correct: Creates a new filtered array })) })); function ItemList() { const { items, addItem, removeItem } = useStore(); return ( <ul> {items.map(item => ( <li key={item.id}> {item.name} <button onClick={() => removeItem(item.id)}>Remove</button> </li> ))} </ul> ); } export default ItemList; """ ## 3. Code Splitting and Lazy Initialization ### Standard 7: Lazy Load Zustand Stores **Do This:** Use dynamic imports or React's "lazy" function to load Zustand stores only when they are needed. **Don't Do This:** Import all Zustand stores eagerly at the top level of your application. This increases the initial load time of the application. **Why This Matters:** Code splitting divides your application into smaller chunks, which can be loaded on demand. Lazy loading Zustand stores reduces the initial JavaScript bundle size, improving the initial load time and perceived performance. **Code Example:** """javascript import React, { lazy, Suspense } from 'react'; const LazyUserProfile = lazy(() => import('./UserProfile')); function App() { return ( <Suspense fallback={<div>Loading...</div>}> <LazyUserProfile /> </Suspense> ); } export default App; """ Where "UserProfile.js" uses a Zustand store: """javascript // UserProfile.js import { create } from 'zustand'; const useStore = create((set) => ({ name: 'John Doe', age: 30, })); function UserProfile() { const { name, age } = useStore(); return ( <div> <p>Name: {name}</p> <p>Age: {age}</p> </div> ); } export default UserProfile; """ ### Standard 8: Chunk Large Stores **Do This:** If a single Zustand store contains a large amount of data, consider breaking it down into smaller, more manageable stores. **Don't Do This:** Store a massive, monolithic state object in a single Zustand store, as this can lead to performance issues and make the application harder to maintain. **Why This Matters:** Dividing a large store into smaller stores isolates state updates and reduces the scope of rerenders. It improves organization and makes the application more scalable. ## 4. Avoiding Anti-Patterns ### Standard 9: Avoid Over-Storing Derived State **Do This:** Calculate derived state within components or with memoized selectors instead of storing redundant derived data in the Zustand store. **Don't Do This:** Store derived data directly in the store if it can be efficiently computed from existing state. This can lead to unnecessary updates and increased complexity. **Why This Matters:** Storing derived data can introduce inconsistencies and complex update logic. Calculating derived data on demand or with memoized selectors keeps the store simple and avoids redundant updates. **Code Example:** """javascript import { create } from 'zustand'; import { useMemo } from 'react'; const useStore = create((set) => ({ items: [ { id: 1, name: 'Apple', price: 1.0, quantity: 2 }, { id: 2, name: 'Banana', price: 0.5, quantity: 5 }, ], })); function ShoppingCart() { const items = useStore(state => state.items); // Correct: derived state calculated using useMemo. const totalCost = useMemo(() => { return items.reduce((acc, item) => acc + item.price * item.quantity, 0); }, [items]); return ( <div> <ul> {items.map(item => ( <li key={item.id}> {item.name} - ${item.price} x {item.quantity} </li> ))} </ul> <p>Total Cost: ${totalCost}</p> </div> ); } export default ShoppingCart; """ ### Standard 10: Detach Long-Running Operations **Do This:** For long-running operations or side effects, use asynchronous actions within the store and, where appropriate, separate the operation entirely (e.g. into a web worker). Avoid blocking the main thread. **Don't Do This:** Perform synchronous, long-running operations directly within the "set" function; this will block the UI and degrade the user experience. **Why This Matters:** Blocking the main thread causes the UI to freeze and become unresponsive during long calculations and operations. By using asynchronous actions and potentially web workers, you can keep the UI responsive. **Code Example:** """javascript import { create } from 'zustand'; const useStore = create((set) => ({ data: null, loading: false, fetchData: async () => { set({ loading: true }); try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); set({ data, loading: false }); } catch (error) { console.error('Error fetching data:', error); set({ loading: false, data: null }); // handle the error case correctly } }, })); function DataDisplay() { const { data, loading, fetchData } = useStore(); useEffect(() => { fetchData(); }, [fetchData]); if (loading) { return <div>Loading...</div>; } if (!data) { return <div>Error loading data.</div>; } return ( <div> <p>Data: {JSON.stringify(data)}</p> </div> ); } export default DataDisplay; """ ## 5. Specific Zustand Features ### Standard 11: Using the "devtools" Middleware (Development Only) **Do This:** Wrap your Zustand store creation with the "devtools" middleware during development to enable time-travel debugging and state inspection. Ensure this middleware is removed or disabled in production builds. **Don't Do This:** Leave the "devtools" middleware enabled in production, as it adds overhead and exposes internal state. **Why This Matters:** The "devtools" middleware enhances the development process by providing valuable insights. Disabling it in production ensures optimal performance and security. **Code Example:** """javascript import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; const useStore = create(devtools((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { name: 'MyStore' })); // Provide a unique name for the store function Counter() { const { count, increment } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } export default Counter; """ In production: """javascript import { create } from 'zustand'; // devtools middleware removed const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); function Counter() { const { count, increment } = useStore(); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } export default Counter; """ ### Standard 12: Integrate with Persist Middleware Sparingly **Do This:** Use the "persist" middleware judiciously, only for essential data that needs to be persisted across sessions. Consider encryption for sensitive data. **Don't Do This:** Persist large amounts of unimportant or frequently changing data, as this affects application performance and storage usage. **Why This Matters:** Persistent storage can impact application performance and create potential security vulnerabilities and privacy concerns. Use with care. **Code Example:** """javascript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; const useStore = create(persist((set) => ({ token: null, setToken: (token) => set({ token }), removeToken: () => set({ token: null }), }), { name: 'auth-storage', // unique name storage: createJSONStorage(() => localStorage), partialize: (state) => ({ token: state.token }), // Only persist the token } )); function AuthComponent() { const { token, setToken, removeToken } = useStore(); return ( // ... ); } export default AuthComponent; """ ## 6. Testing and Monitoring ### Standard 13: Monitor Store Performance **Do This:** Use performance monitoring tools (e.g., React Profiler, browser developer tools) to identify performance bottlenecks related to Zustand stores. Track render times and update frequencies. **Don't Do This:** Assume your Zustand stores are performant without proper monitoring and profiling. **Why This Matters:** Monitoring is essential for identifying and addressing performance issues. Identifying slow selectors, excessive rerenders, and other issues allows for targeted optimization. ### Standard 14: Write Targeted Tests **Do This:** Write unit tests to verify the behavior of your Zustand stores. Focus on testing state transitions and selector logic. **Don't Do This:** Neglect unit testing of the Zustand store logic. **Why This Matters:** Testing ensures that your Zustand stores function correctly and reliably. Writing specific tests for selectors and state transitions can prevent regressions and improve the overall quality of the application. ## 7. TypeScript Considerations ### Standard 15: Leverage TypeScript for Type Safety **Do This:** Use TypeScript to define precise types for your Zustand store's state and actions. Ensure that all actions are type-safe and handle edge cases appropriately. **Don't Do This:** Use "any" liberally or avoid defining types. This sacrifices the benefits of TypeScript and can lead to runtime errors. **Why This Matters:** TypeScript provides static type checking, which can help catch errors early in the development cycle, improving code quality and maintainability. **Code Example:** """typescript import { create } from 'zustand'; interface UserState { id: number; name: string; email: string; } interface UserActions { updateName: (name: string) => void; updateEmail: (email: string) => void; } const useUserStore = create<UserState & UserActions>((set) => ({ id: 1, name: 'John Doe', email: 'john.doe@example.com', updateName: (name: string) => set({ name }), updateEmail: (email: string) => set({ email }), })); export default useUserStore; """ By following these performance optimization standards, developers can create efficient and performant React applications leveraging the power of Zustand.
# Testing Methodologies Standards for Zustand This document outlines the coding standards for testing methodologies when using Zustand for state management in React applications. It aims to provide clear guidelines and best practices for unit, integration, and end-to-end testing, specifically tailored for Zustand's unique features and capabilities. Following these standards ensures maintainability, reliability, and performance of state management logic. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) * **Do This:** Embrace TDD by writing tests *before* implementing functionality. This helps to clarify requirements and ensures thorough testing from the outset. * **Don't Do This:** Neglect writing tests until after the implementation is complete. This often leads to incomplete or poorly designed tests. **Why:** TDD shifts the focus towards well-defined requirements and facilitates the construction of more testable and maintainable code. ### 1.2. Test Pyramid * **Do This:** Follow the structure of the test pyramid: more unit tests, fewer integration tests, and even fewer end-to-end tests. Unit tests should form the base, providing rapid feedback and excellent coverage of individual components and Zustand stores. * **Don't Do This:** Rely heavily on end-to-end tests at the expense of unit and integration tests. This makes debugging slower and more complex. **Why:** A balanced testing strategy ensures comprehensive coverage while optimizing for speed and maintainability. Unit tests are faster and more focused, while integration and E2E tests are more holistic but slower. ### 1.3. Test Naming Conventions * **Do This:** Use clear and descriptive test names. Use a standardized convention like "describe('ComponentName/Store Slice - Unit of Work', () => { ... });" or "it('should do something', () => { ... });". Consider BDD-style naming ("should do X when Y"). * **Don't Do This:** Use vague or ambiguous test names that don't clearly indicate the purpose of the test. Avoid names like "test1", "test2", etc. **Why:** Clear test names make it easier to understand the functionality being tested and quickly identify the cause of failures. ### 1.4. Test Isolation * **Do This:** Ensure tests are isolated from each other. Use mocking and stubbing judiciously to control dependencies and prevent tests from interfering with each other’s state. Zustand stores are particularly susceptible to state bleeding, so use "store.setState()" or a store reset function to clean up after each test. * **Don't Do This:** Allow tests to share state or rely on specific execution order. This can lead to flaky and unreliable test results. **Why:** Isolated tests are more reliable, reproducible, and easier to debug. ## 2. Unit Testing Zustand Stores ### 2.1. Store Structure and Testing Scope * **Do This:** Unit test each slice or logical unit of your Zustand store independently. This allows precise focus on individual state mutations and actions. Consider how the store is sliced and define test suites related to each slice. * **Don't Do This:** Create "god" tests that attempt to test the entire store in a single test case. This is brittle and difficult to maintain. **Why:** Focusing on individual slices promotes modularity and easier debugging. ### 2.2. Testing State Mutations * **Do This:** Verify that actions correctly modify the store's state. Use assertions to check the state before and after actions are called. * **Don't Do This:** Only focus on testing the return value of actions without validating the resulting state. **Why:** The primary responsibility of a Zustand store is to manage state effectively. Tests should validate state transitions. **Example:** """javascript import { create } from 'zustand'; import { describe , it, expect, beforeEach } from 'vitest'; // Or use Jest import { act } from 'react-dom/test-utils'; const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }) })); describe('Counter Store', () => { let store; beforeEach(() => { store = createCounterStore(); }); it('should initialize count to 0', () => { const { count } = store.getState(); expect(count).toBe(0); }); it('should increment the count', () => { act(() => { store.getState().increment(); }); const { count } = store.getState(); expect(count).toBe(1); }); it('should decrement the count', () => { act(() => { store.getState().decrement(); }); const { count } = store.getState(); expect(count).toBe(-1); }); it('should reset the count to 0', () => { act(() => { store.getState().increment(); store.getState().reset(); }); const { count } = store.getState(); expect(count).toBe(0); }); }); """ ### 2.3. Testing Actions and Side Effects * **Do This:** If actions trigger side effects (e.g., API calls), mock the dependencies to control the external environment. Use "jest.mock" (or equivalent in other testing frameworks) to mock API functions. Ensure actions correctly handle both success and error scenarios. * **Don't Do This:** Allow tests to make actual API calls, as this can lead to unpredictable results and slow test execution. **Why:** Mocking allows you to isolate the store's logic and simulate different scenarios without relying on external systems. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the API service const mockApiService = { fetchData: vi.fn() }; const createDataStore = () => create((set) => ({ data: null, loading: false, error: null, fetchData: async () => { set({ loading: true, error: null }); try { const data = await mockApiService.fetchData(); set({ data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } } })); describe('Data Store', () => { let store; beforeEach(() => { store = createDataStore(); }); it('should initialize with default values', () => { const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should set loading to true when fetching data', async () => { mockApiService.fetchData.mockResolvedValue({}); // Resolve immediately so loading changes. act(() => { // Wrap store changes. store.getState().fetchData(); }); expect(store.getState().loading).toBe(true); }); it('should fetch data successfully', async () => { const mockData = { name: 'Test Data' }; mockApiService.fetchData.mockResolvedValue(mockData); await act(async () => { await store.getState().fetchData(); // Wait for async function to complete. }); const { data, loading, error } = store.getState(); expect(data).toEqual(mockData); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should handle errors when fetching data', async () => { mockApiService.fetchData.mockRejectedValue(new Error('Failed to fetch')); await act(async () => { await store.getState().fetchData(); // Await promise. }); const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBe('Failed to fetch'); }); }); """ ### 2.4. Testing Selectors * **Do This:** If the store uses selectors to derive data, write unit tests to verify that the selectors return the correct values based on different store states. * **Don't Do This:** Assume that selectors always work correctly without dedicated testing. **Why:** Selectors are an important part of the store logic, and testing them ensures that derived data is accurate. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect } from 'vitest'; import { act } from 'react-dom/test-utils'; const createItemStore = () => create((set, get) => ({ items: [{ id: 1, name: 'Item 1', price: 10 }, { id: 2, name: 'Item 2', price: 20 }], totalPrice: () => { const items = get().items; return items.reduce((sum, item) => sum + item.price, 0); }, addItem: (item) => set(state => ({items: [...state.items, item]})) })); describe('Item Store', () => { it('should calculate the total price correctly', () => { const store = createItemStore(); const totalPrice = store.getState().totalPrice(); expect(totalPrice).toBe(30); }); it('should update total price when an item is added', () => { const store = createItemStore(); const initialTotalPrice = store.getState().totalPrice(); const newItem = { id: 3, name: "New Item", price: 5 }; act(() => { store.getState().addItem(newItem); }); const newTotalPrice = store.getState().totalPrice(); expect(newTotalPrice).toBe(initialTotalPrice + newItem.price); // 30 + 5 = 35 }); }); """ ### 2.5. Testing with Time * **Do This:** When dealing with asynchronous operations and timers (e.g., debouncing, throttling, timeouts), leverage tools like "jest.useFakeTimers()". This creates controlled and predictable tests. * **Don't Do This:** Rely on real-time delays in tests, which can make tests slow and unreliable. **Why:** Fake timers allow you to fast-forward time and reliably test asynchronous interactions. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { act } from 'react-dom/test-utils'; const createDebouncedStore = () => create(set => ({ value: '', setValue: vi.fn((newValue) => { // This vi.fn allows you to see if setValue has been called set({value: newValue}) }), // Mock setValue for testing purposes debouncedSetValue: (newValue) => { debounceSetValueInternal(newValue,set); } })); let debounceSetValueInternal = (newValue,set) => { return setTimeout(() => { set({ value: newValue }); }, 300); }; describe('Debounced Store', () => { beforeEach(() => { vi.useFakeTimers(); // Enable fake timers. Don't use modern; use a basic fake timer implementation if you intend to use setTimeout. }); afterEach(() => { vi.useRealTimers(); }); it('should update the value after the debounce delay', () => { const store = createDebouncedStore(); act(() => { store.getState().debouncedSetValue('test'); }); // Fast-forward time by 300ms vi.advanceTimersByTime(300); // Use advanceTimersByTime expect(store.getState().value).toBe('test'); }); }); """ ### 2.6 Zustand-Specific Hooks and Utilities * **Do This:** When testing components that use the Zustand store directly through hooks, test the behavior of effects, selectors, and actions initiated through the hook. * **Don't Do This:** Neglect to test how your components interact with the state in the store. **Why:** Zustand's hook-based approach encourages testing state transformations and side effects that are triggered in components. ## 3. Integration Testing with React Components ### 3.1. Component Interactions with Zustand * **Do This:** Test how React components interact with the Zustand store. Use testing libraries like React Testing Library to simulate user interactions and verify that the store is updated correctly. * **Don't Do This:** Focus solely on the component's rendering output without validating its integration with the store. **Why:** Integration tests bridge the gap between unit tests and end-to-end tests, verifying that components and the store work together as expected. **Example:** """javascript import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { create } from 'zustand'; import { describe, it, expect, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the Zustand store const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); function CounterComponent() { const [count, increment] = createCounterStore((state) => [state.count, state.increment]); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } describe('Counter Component', () => { beforeEach(() => { createCounterStore.setState({ count: 0 }); // Reset state before each test. Important b/c store can be shared. }); it('should display the initial count', () => { render(<CounterComponent />); const countElement = screen.getByText('Count: 0'); expect(countElement).toBeInTheDocument(); }); it('should increment the count when the button is clicked', async () => { const user = userEvent.setup() render(<CounterComponent />); const incrementButton = screen.getByText('Increment'); await user.click(incrementButton); const countElement = screen.getByText('Count: 1'); expect(countElement).toBeInTheDocument(); }); }); """ ### 3.2. Mocking Zustand for Isolated Component Tests * **Do This:** For component-specific tests where you want to isolate the component and avoid relying on the real Zustand store, mock the store or its hook usage. This can be achieved using "jest.mock" to replace the store's hook with a mock implementation. * **Don't Do This:** Unnecessarily test the Zustand store's internal logic within component tests. Component tests should primarily focus on the component's behavior given a specific state. **Why:** Mocking the store allows you to control the state injected into the component and test different scenarios in isolation. This is useful when the component interacts with a complex store and you only need to test a specific aspect of its behavior. ### 3.3. Testing Component Side Effects * **Do This:** Verify that components correctly trigger store actions in response to user interactions or lifecycle events. Spy functions (e.g., "jest.spyOn") can be used to assert that actions are called with the expected parameters. * **Don't Do This:** Neglect to test side effects triggered by components, as these are often a critical part of the application's functionality. **Why:** Components are responsible for initiating state changes, and testing these interactions ensures that the store is updated correctly in response to user actions. ## 4. End-to-End (E2E) Testing ### 4.1. Realistic User Flows * **Do This:** End-to-end tests should simulate complete user flows through the application, including interactions with the Zustand store. Use tools like Cypress or Playwright to automate these tests. * **Don't Do This:** Write E2E tests that only cover basic functionality or skip important user interactions. **Why:** E2E tests provide a high level of confidence that the application works correctly in a real-world environment. ### 4.2. State Validation in E2E Tests * **Do This:** Include assertions to validate that the Zustand store is updated correctly as users interact with the application. This can be done by querying the UI for updated values that reflect the store's state. * **Don't Do This:** Rely solely on visual checks without validating the underlying state changes. **Why:** Validating the store's state in E2E tests ensures that the application's data is consistent and that user interactions have the expected effect on the state. ### 4.3. E2E Test Environment * **Do This:** Set up a dedicated test environment for E2E tests, including seed data and mock services to ensure consistent and predictable results. * **Don't Do This:** Run E2E tests against production environments, which can be unpredictable and potentially disruptive. **Why:** Dedicated test environments provide a controlled environment for E2E tests, ensuring that they are reliable and reproducible. ## 5. Common Anti-Patterns * **Over-mocking:** Avoid mocking everything. Mocking too much can lead to tests that are too far removed from reality and don't catch actual integration issues. * **Ignoring edge cases:** Ensure tests cover edge cases and boundary conditions to prevent unexpected behavior. * **Flaky tests:** Address flaky tests immediately. Flaky tests undermine confidence in the test suite and make it difficult to identify actual issues. Retries can be a workaround, but the core issue must be resolved. * **Lack of cleanup:** Always clean up after tests, especially when dealing with Zustand stores. Reset the store's state or mock dependencies to prevent tests from interfering with each other. * **Testing implementation details:** Tests should focus on the behavior of the store and components, not on the implementation details. Avoid asserting on private variables or internal function calls. ## 6. Technology-Specific Considerations * **React Testing Library:** Encourages testing from the user's perspective, focusing on what the user sees and interacts with. Avoid implementation-specific selectors. * **Vitest/Jest:** Popular JavaScript testing frameworks with excellent support for mocking, spying, and asynchronous testing. Use "jest.mock" for mocking dependencies. Vitest is generally faster for Vite-based projects; Jest is more widely known and used. * **Cypress/Playwright:** Powerful E2E testing tools that allow you to automate browser interactions and validate the application's behavior. By adhering to these coding standards, development teams can create robust and reliable Zustand-based applications with confidence, ensuring that state management logic is thoroughly tested and maintainable.