# 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 (
{userData.name}
<p>{userData.email}</p>
);
};
const UpdateProfile = () => {
const updateUser = useStore(state => state.updateUser);
const [name, setName] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
updateUser({name});
}
return (
setName(e.target.value)} />
Update Name
);
};
"""
"""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 (
{userData.name}
<p>{userData.email}</p>
setName(e.target.value)} />
Update Name
);
};
"""
### 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 (
{user.name}
<p>{user.email}</p>
);
};
"""
"""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 (
{user.name}
<p>{user.email}</p>
);
};
"""
### 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 }) => (
{children}
);
const Input = ({ type, value, onChange }) => (
);
const Form = () => {
const [email, setEmail] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
alert("Email submitted: ${email}");
};
return (
setEmail(e.target.value)} />
Submit
);
};
"""
## 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 (
{todos.map(todo => (
toggleTodo(todo.id)} />
{todo.text}
))}
);
};
"""
"""jsx
// Bad: Subscribing to the entire store (BAD PRACTICE)
const TodoListBad = () => {
const { todos, toggleTodo } = useTodoStore(); // Gets EVERYTHING
return (
{todos.map(todo => (
toggleTodo(todo.id)} />
{todo.text}
))}
);
};
"""
### 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 (
<p>Count: {count}</p>
Increment
Log count
);
};
"""
### 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 (
Name: setName(e.target.value)} />
Email: setEmail(e.target.value)} />
Update Profile
);
};
"""
### 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 (
{products.map(product => (
{product.name} - ${product.price}
))}
);
};
"""
## 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();
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((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.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Code Style and Conventions Standards for Zustand This document outlines the code style and conventions standards for Zustand, a minimalist state management library for React applications. Adhering to these standards promotes consistency, readability, maintainability, and performance across our projects. These guidelines are designed for the latest versions of Zustand and incorporate modern best practices. ## 1. General Formatting and Style Maintaining a consistent code style is crucial for readability and collaboration. These standards apply to all code within Zustand-managed components and stores. ### 1.1. Formatting Tools * **Do This:** Use Prettier and ESLint to automatically format and lint your code. Configure them with a shared configuration file across the team to ensure consistency. * **Don't Do This:** Rely on manual formatting or inconsistent editor settings. **Why:** Automated formatting eliminates subjective style debates and ensures consistent code appearance across the project. It prevents style-related distractions during code reviews. **Example:** """javascript // .prettierrc.js module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; // .eslintrc.js module.exports = { extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], plugins: ['react', 'prettier'], parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true, }, }, rules: { 'prettier/prettier': 'error', 'react/prop-types': 'off', // Consider enabling for specific projects }, settings: { react: { version: 'detect', }, }, env: { browser: true, node: true, es6: true, }, }; """ ### 1.2. Indentation and Spacing * **Do This:** Use 2 spaces for indentation. * **Do This:** Maintain consistent spacing around operators, after commas, and within curly braces. **Why:** Consistent indentation and spacing improve readability and reduce visual clutter. **Example:** """javascript // Correct const myVariable = { prop1: value1, prop2: value2 }; const result = a + b * c; // Incorrect const myVariable={prop1:value1,prop2:value2}; const result=a+b*c; """ ### 1.3. Line Length * **Do This:** Limit lines to a maximum of 120 characters. * **Do This:** Break long lines into multiple shorter lines to improve readability. **Why:** Shorter lines are easier to read, especially on smaller screens or when code is displayed side-by-side. **Example:** """javascript // Correct const veryLongVariableName = someFunction( argument1, argument2, argument3, ); // Incorrect const veryLongVariableName = someFunction(argument1, argument2, argument3); """ ### 1.4. Comments * **Do This:** Write clear and concise comments to explain complex logic or non-obvious code. * **Do This:** Use JSDoc-style comments to document functions, components, and variables. * **Don't Do This:** Write obvious or redundant comments that describe what the code is already doing. **Why:** Comments improve code maintainability and help other developers (including your future self) understand the code's purpose and functionality. **Example:** """javascript /** * Calculates the total price of items in the cart. * * @param {Array<Object>} cartItems An array of cart items. * @returns {number} The total price. */ const calculateTotalPrice = (cartItems) => { // Reduce the cart items array to calculate the total price. return cartItems.reduce((total, item) => total + item.price * item.quantity, 0); }; """ ## 2. Zustand Specific Conventions These conventions apply specifically to Zustand stores and the code that interacts with them. ### 2.1. Store Definition * **Do This:** Define stores using the "create" function from Zustand. * **Do This:** Keep store definitions concise and focused on the state and actions that modify it. * **Do This:** Organize actions within the store for clarity. **Why:** The "create" function provides a consistent and idiomatic way to define Zustand stores. Well-organized stores are easier to understand and maintain. **Example:** """javascript import { create } from 'zustand'; const useBearStore = create((set) => ({ bears: 0, increaseBears: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })); export default useBearStore; """ ### 2.2. Naming Conventions * **Do This:** Use the "use" prefix for custom hooks that consume Zustand stores (e.g., "useBearStore"). * **Do This:** Name store properties and actions descriptively and consistently. Use camelCase. * **Do This:** For asynchronous actions, consider using a "fetch" or "load" prefix (e.g., "fetchUserData", "loadProducts"). **Why:** Consistent naming conventions improve code discoverability and make it easier to understand the purpose of different parts of the code. **Example:** """javascript import { create } from 'zustand'; const useUserStore = create((set) => ({ userData: null, isLoading: false, error: null, fetchUserData: async (userId) => { set({ isLoading: true, error: null }); try { const response = await fetch("/api/users/${userId}"); const data = await response.json(); set({ userData: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useUserStore; """ ### 2.3. Immutability * **Do This:** Update state immutably within Zustand stores. This is handled inherently by Zustand's "set" function, but be mindful when dealing with nested objects or arrays. * **Don't Do This:** Directly modify the state object. **Why:** Immutability ensures predictable state updates and prevents unexpected side effects. Zustand uses shallow equality checks, so it expects immutable updates. Modifying state directly will lead to components not re-rendering when they should. **Example:** """javascript // Correct (using the functional update form) set(state => ({ items: [...state.items, newItem] })); // or Immer import { create } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; const useStore = create(immer((set) => ({ items: [], addItem: (newItem) => set((state) => { state.items.push(newItem); // Immer handles immutability }), }))); // Incorrect (direct mutation) //state.items.push(newItem); // Don't do this! """ ### 2.4. Selectors * **Do This:** Use selectors to derive specific values from the store. * **Do This:** Memoize selectors to prevent unnecessary re-renders. You can use "useMemo" or libraries like "reselect". **Why:** Selectors optimize performance by preventing components from re-rendering when the specific data they depend on hasn't changed. **Example:** """javascript import useBearStore from './useBearStore'; import { useMemo } from 'react'; function BearCounter() { const bears = useBearStore((state) => state.bears); // Direct selection const doubleBears = useMemo(() => bears * 2, [bears]); // Memoized selector return <h1>{doubleBears}</h1>; } """ ### 2.5. Middleware * **Do This:** Use Zustand middleware to add common functionality like persistence, logging, or Immer integration. * **Do This:** Keep middleware functions modular and reusable. * **Don't Do This:** Implement complex logic directly within the store definition if it can be separated into middleware. **Why:** Middleware promotes separation of concerns and makes it easier to add cross-cutting functionality to Zustand stores. **Example:** """javascript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; const useBoundStore = create( persist( (set) => ({ fishes: 0, addAFish: () => set((state) => ({ fishes: state.fishes + 1 })), }), { name: 'food-storage', // unique name for the storage getStorage: () => localStorage, // (optional) by default the 'localStorage' is used }, ), ); export default useBoundStore; """ ### 2.6. Slices * **Do This:** Use slices for large stores. Slices make your store more manageable and testable * **Do This:** Group related state and actions into individual slices. * **Don't Do This:** Create extremely large stores without considering how to organize them. **Why:** Slices improve the organization of large stores by breaking them down into smaller, more manageable units. **Example:** """javascript import { create } from 'zustand'; const createCounterSlice = (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }); const createUserSlice = (set) => ({ user: null, setUser: (user) => set({ user }), }); const useStore = create((...a) => ({ ...createCounterSlice(...a), ...createUserSlice(...a), })); export default useStore; """ ### 2.7. Async Actions * **Do This:** Handle loading and error states within asynchronous actions. * **Do This:** Use "try...catch" blocks to handle errors gracefully. * **Do This:** Name asynchronous actions to clearly indicate their purpose. **Why:** Proper error handling and loading state management provide a better user experience and prevent unexpected behavior. **Example:** """javascript import { create } from 'zustand'; const useProductsStore = create((set) => ({ products: [], isLoading: false, error: null, fetchProducts: async () => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/products'); const data = await response.json(); set({ products: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, })); export default useProductsStore; """ ### 2.8 Type Safety with TypeScript * **Do This**: Use TypeScript to define the state type for stores * **Do This**: Ensure all action definitions are properly typed * **Do This**: Create a separate type definition file for complex state structures **Why**: TypeScript provides type safety, improves code maintainability and provides better intellisense inside your IDE **Example**: """typescript import { create } from 'zustand' interface BearState { bears: number increasePopulation: () => void removeAllBears: () => void } const useBearStore = create<BearState>((set) => ({ bears: 0, increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) export default useBearStore """ ## 3. Component Integration These standards focus on how Zustand stores are used within React components. ### 3.1. Selective Updates * **Do This:** Use the selector function provided by Zustand to subscribe only to the parts of the state that the component needs. * **Don't Do This:** Subscribe to the entire store if the component only needs a small piece of data. **Why:** Selective updates prevent unnecessary re-renders and improve component performance. **Example:** """javascript import useBearStore from './useBearStore'; function BearCounter() { const bears = useBearStore((state) => state.bears); // Select only the 'bears' property return <h1>{bears}</h1>; } """ ### 3.2. Avoiding Prop Drilling * **Do This:** Use Zustand to avoid prop drilling, especially for deeply nested components. * **Don't Do This:** Pass state and actions through multiple levels of components if they are only needed in a specific child component. **Why:** Zustand provides a centralized state management solution that eliminates the need to pass props through multiple levels of the component tree. **Example:** """javascript // Before (prop drilling) function Grandparent({ bears, increaseBears }) { return <Parent bears={bears} increaseBears={increaseBears} />; } function Parent({ bears, increaseBears }) { return <Child bears={bears} increaseBears={increaseBears} />; } function Child({ bears, increaseBears }) { return ( <> <h1>{bears} Bears</h1> <button onClick={increaseBears}>Increase</button> </> ); } // After (using Zustand) import useBearStore from './useBearStore'; function Grandparent() { return <Parent />; } function Parent() { return <Child />; } function Child() { const { bears, increaseBears } = useBearStore(); return ( <> <h1>{bears} Bears</h1> <button onClick={increaseBears}>Increase</button> </> ); } """ ### 3.3 Extracting Logic * **Do This:** Place complex logic inside the store definition. * **Don't Do This:** Implement complex logic inside components that consume Zustand stores if it is related to state updates. **Why:** Extracting logic keeps components clean and focused on rendering, making them easier to understand and test. **Example:** """javascript //Complex logic inside the store import { create } from 'zustand'; const useCartStore = create((set) => ({ cartItems: [], addItemToCart: (item) => set((state) => { const existingItem = state.cartItems.find((cartItem) => cartItem.id === item.id); if (existingItem) { return { cartItems: state.cartItems.map((cartItem) => cartItem.id === item.id ? { ...cartItem, quantity: cartItem.quantity + 1 } : cartItem ), }; } else { return { cartItems: [...state.cartItems, { ...item, quantity: 1 }] }; } }), })); """ ## 4. Performance Optimization These standards focus on optimizing the performance of Zustand-managed applications. ### 4.1. Batching Updates * **Do This:** Use the "batch" function from "react-dom" or "ReactDOM.unstable_batchedUpdates" to batch multiple state updates into a single re-render. **Why:** Batching updates reduces the number of re-renders, improving performance, especially when multiple state changes occur in quick succession. **Example:** """javascript import { unstable_batchedUpdates } from 'react-dom'; import useMyStore from './useMyStore'; function MyComponent() { const { updateValue1, updateValue2 } = useMyStore(); const handleClick = () => { unstable_batchedUpdates(() => { updateValue1('new value 1'); updateValue2('new value 2'); }); }; return <button onClick={handleClick}>Update Values</button>; } """ ### 4.2. Avoiding Unnecessary Re-renders * **Do This:** Use "useMemo" and "useCallback" to memoize expensive calculations and function references. * **Do This:** Ensure components only re-render when their dependencies change. **Why:** Memoization prevents unnecessary re-renders, improving performance, especially for complex components. **Example:** """javascript import { useMemo, useCallback } from 'react'; import useMyStore from './useMyStore'; function MyComponent({ data }) { const { updateValue } = useMyStore(); const expensiveCalculation = useMemo(() => { // Perform a complex calculation based on 'data' return data.reduce((acc, value) => acc + value, 0); }, [data]); const handleClick = useCallback(() => { updateValue(expensiveCalculation); }, [updateValue, expensiveCalculation]); return <button onClick={handleClick}>Update Value</button>; } """ ### 4.3. Debouncing and Throttling * **Do This:** Use debouncing or throttling techniques for actions that are triggered frequently, such as input changes or scroll events. **Why:** Debouncing and throttling limit the number of times an action is executed, preventing performance issues when dealing with high-frequency events. **Example:** """javascript import { debounce } from 'lodash'; import useMyStore from './useMyStore'; function MyComponent() { const { setSearchQuery } = useMyStore(); const debouncedSetSearchQuery = debounce((query) => { setSearchQuery(query); }, 300); const handleInputChange = (event) => { debouncedSetSearchQuery(event.target.value); }; return <input type="text" onChange={handleInputChange} />; } """ ## 5. Testing These conventions apply to testing Zustand stores and components that use them. ### 5.1. Unit Testing * **Do This:** Write unit tests for Zustand stores to verify that state updates and actions behave as expected. * **Do This:** Mock external dependencies and side effects in unit tests. **Why:** Unit tests ensure the correctness and reliability of Zustand stores. **Example:** """javascript import useBearStore from './useBearStore'; import { act } from 'react-dom/test-utils'; describe('useBearStore', () => { it('should initialize with the correct default value', () => { const { result } = renderHook(() => useBearStore()); expect(result.current.bears).toBe(0); }); it('should increase the bear population', () => { const { result } = renderHook(() => useBearStore()); act(() => { result.current.increaseBears(); }); expect(result.current.bears).toBe(1); }); it('should remove all bears', () => { const { result } = renderHook(() => useBearStore()); act(() => { result.current.increaseBears(); result.current.removeAllBears(); }); expect(result.current.bears).toBe(0); }); }); """ ### 5.2. Integration Testing * **Do This:** Write integration tests to verify that components interact correctly with Zustand stores. * **Do This:** Test the behavior of components in different scenarios and with different data. **Why:** Integration tests ensure that components and Zustand stores work together seamlessly. ### 5.3. End-to-End Testing * **Do This:** Use end-to-end (E2E) tests to verify the overall functionality of the application, including Zustand store integration. * **Do This:** Ensure that E2E tests cover critical user flows and interactions. **Why:** E2E tests provide confidence that the application works as expected from the user's perspective. ## 6. Security Best Practices Although Zustand itself isn't inherently a security risk, how you manage the data within your stores and how you interact with external APIs can introduce vulnerabilities. ### 6.1. Data Sanitization * **Do This:** Sanitize any data received from external sources (e.g., user input, API responses) before storing it in Zustand stores. * **Do This:** Follow secure coding practices to prevent cross-site scripting (XSS) and other injection attacks. **Why:** Data sanitization prevents malicious data from being stored and displayed, mitigating security risks. ### 6.2. Authentication and Authorization * **Do This:** Implement proper authentication and authorization mechanisms to protect sensitive data stored in Zustand stores. * **Do This:** Store sensitive information securely (e.g., using encryption). **Why:** Authentication and authorization restrict access to sensitive data, preventing unauthorized access and data breaches. ### 6.3. Secure API Communication * **Do This:** Use HTTPS for all API communication to encrypt data in transit. * **Do This:** Validate API responses to ensure they are well-formed and contain the expected data. **Why:** Secure API communication protects data from eavesdropping and tampering. ### 6.4. Preventing State Tampering * **Do This:** Be aware that client-side state can potentially be tampered with. Don't rely solely on client-side state for critical security decisions. * **Do This:** Perform server-side validation for important operations. **Why:** Client-side state is vulnerable to manipulation, so server-side validation is essential for ensuring data integrity. Don't store sensitive information (like API keys) directly in the Zustand store if it can be avoided. ## 7. Tooling Recommendations * **ESLint:** With plugins for React, TypeScript, and Prettier to enforce code style and best practices. * **Prettier:** To keep consistent code formatting. * **React Developer Tools:** To inspect component state and props, including Zustand store data. * **Testing Libraries:** Jest and React Testing Library provide excellent tooling for unit and integration testing. By adhering to these coding standards and conventions, we can ensure that our Zustand-managed applications are consistent, readable, maintainable, performant, and secure. This leads to higher-quality software and improved team collaboration.
# Deployment and DevOps Standards for Zustand This document outlines the coding standards for Zustand specifically concerning deployment and DevOps practices. These guidelines aim to ensure maintainable, performant, and secure state management in production environments utilizing Zustand. ## 1. Build Processes, CI/CD, and Production Considerations for Zustand ### 1.1. Standards * **DO** utilize environment variables to configure Zustand stores for different environments (development, staging, production). * **WHY:** Environment variables allow for dynamic configuration without modifying code, separating configuration from code, enhancing portability, and improving security by avoiding hardcoding sensitive information. * **DON'T** hardcode environment-specific values directly into Zustand store definitions. * **WHY:** This tightly couples the code to specific environments, making it difficult to deploy or maintain and increases the risk of errors when transitioning between environments. * **DO** integrate Zustand store initialization into your CI/CD pipeline using build-time or runtime configuration. * **WHY:** Automating the configuration process ensures consistency across deployments and reduces the risk of manual configuration errors. * **DON'T** rely on manual configuration of Zustand stores in production. * **WHY:** Manual configuration is prone to errors, difficult to track, and inconsistent. * **DO** implement feature flags to enable or disable Zustand-related functionality without redeploying code. * **WHY:** Feature flags provide a mechanism for gradual rollouts, A/B testing, and emergency shutdowns, which reduces risk and improves agility. * **DON'T** deploy breaking changes to Zustand store structures without proper migration strategies and feature flags. * **WHY:** Breaking changes can lead to application errors and data loss. Feature flags allow for controlled and gradual rollouts, mitigating the impact of breaking changes. * **DO** use proper semantic versioning to track changes to your Zustand stores and their interactions/dependencies within components. * **WHY:** Semantic Versioning (SemVer) allows for clarity across teams when deploying or referencing code that relies on the store, to minimize potential errors. ### 1.2. Code Examples """typescript // Example: Using environment variables to configure Zustand store import { create } from 'zustand'; interface MyStoreState { apiEndpoint: string; featureXEnabled: boolean; } interface MyStoreActions { initialize: () => void; } type MyStore = MyStoreState & MyStoreActions; const useMyStore = create<MyStore>((set) => ({ apiEndpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || 'https://default-api.com', // Utilize env variable featureXEnabled: process.env.NEXT_PUBLIC_FEATURE_X === 'true', // Utilize env variable initialize: () => { // Any initialization logic that depends on the configuration console.log("Store Initialized with:", { apiEndpoint: process.env.NEXT_PUBLIC_API_ENDPOINT, featureXEnabled: process.env.NEXT_PUBLIC_FEATURE_X }) }, })); // Component using the store function MyComponent() { const apiEndpoint = useMyStore((state) => state.apiEndpoint); const featureXEnabled = useMyStore((state) => state.featureXEnabled); const initialize = useMyStore((state) => state.initialize); useEffect(() => { initialize(); }, [initialize]); return ( <div> <p>API Endpoint: {apiEndpoint}</p> <p>Feature X Enabled: {featureXEnabled ? 'Yes' : 'No'}</p> {/* Component Logic based on featureXEnabled */} </div> ); } export default MyComponent; // .env.production (Example) // NEXT_PUBLIC_API_ENDPOINT=https://production-api.com // NEXT_PUBLIC_FEATURE_X=true """ ### 1.3. Anti-Patterns * **Direct modification of the state outside of the store's "set":** * Directly modifying the state bypasses Zustand's change tracking and can lead to unexpected behavior in production. * **Storing sensitive data directly in the Zustand store, especially client-side:** * Sensitive data in client-side Zustand stores can be exposed to users or malicious actors. Always handle sensitive data securely on the server-side. * **Not handling errors gracefully when updating the Zustand store:** * Unhandled errors can lead to the store not updating correctly, resulting in inconsistent data and application failures. ### 1.4. Performance Considerations * **Optimize Zustand store updates to minimize unnecessary re-renders:** * Use "useMemo" or "useCallback" to prevent unnecessary re-renders when values from Zustand store is used. * **Lazy-load Zustand stores or initialize them only when needed:** * Initializing stores on demand can significantly improve initial load times, especially in large applications. ## 2. Monitoring and Logging for Zustand ### 2.1. Standards * **DO** implement logging for Zustand store updates, especially in production environments. * **WHY:** Logging provides valuable insights into the application's state and helps identify and diagnose issues related to state management. * **DON'T** log sensitive data or personally identifiable information (PII) in your Zustand store logs. * **WHY:** Logging sensitive data can violate privacy regulations and expose users to risk. * **DO** use structured logging formats (e.g., JSON) for Zustand store logs to facilitate analysis and querying. * **WHY:** Structured logging allows for easier filtering, aggregation, and analysis of log data. * **DON'T** rely solely on "console.log" for production logging. * **WHY:** "console.log" is not suitable for production logging due to its lack of structure, filtering capabilities, and integration with centralized logging systems. * **DO** integrate Zustand store monitoring with existing application monitoring tools (e.g., Datadog, New Relic). * **WHY:** Centralized monitoring provides a holistic view of application health and performance, including the Zustand store. * **DON'T** ignore Zustand store-related errors or warnings in your monitoring system. * **WHY:** Errors and warnings can indicate potential issues with the Zustand store and should be investigated promptly. ### 2.2. Code Examples """typescript // Example: Logging Zustand store updates import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; interface MyStoreState { count: number; increment: () => void; } const useMyStore = create<MyStoreState>()( devtools((set) => ({ count: 0, increment: () => { set((state) => { const newState = { count: state.count + 1 }; console.log('Zustand Store Update:', { action: 'increment', newState }); // Logging with context return newState; }); }, })) ); // .env.development // Enable logging by setting DISABLE_ZUSTAND_DEVTOOLS to false or undefined for development env function MyComponent() { const count = useMyStore((state) => state.count); const increment = useMyStore((state) => state.increment); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } export default MyComponent; """ ### 2.3. Anti-Patterns * **Logging overly verbose data in production:** This can negatively impact performance and increase storage costs. Only log essential information. * **Not setting up log rotation or retention policies:** This can lead to log files growing excessively and consuming valuable disk space. * **Ignoring error logs related to Zustand store updates:** This can result in undetected issues and application failures. ### 2.4. Security Considerations * **Sanitize data before logging:** Remove or mask sensitive information to prevent accidental exposure. * **Secure access to log files:** Restrict access to authorized personnel only and protect log files from unauthorized access. * **Regularly review logs for security vulnerabilities and suspicious activity:** This can help detect and prevent potential security breaches. ## 3. Testing and Quality Assurance for Zustand ### 3.1. Standards * **DO** write unit tests for Zustand store actions and selectors. * **WHY:** Unit tests ensure that the store's logic behaves as expected and prevent regressions. * **DON'T** neglect to test Zustand store interactions with components. * **WHY:** Testing component interactions verifies that the store updates correctly trigger re-renders and that components receive the correct data. * **DO** use mocking libraries to isolate Zustand stores during testing. * **WHY:** Mocking allows you to control the store's state and behavior during testing, making tests more predictable and reliable. * **DON'T** rely solely on manual testing for Zustand store functionality. * **WHY:** Manual testing is time-consuming, error-prone, and not scalable for complex applications. * **DO** integrate Zustand store testing into your CI/CD pipeline. * **WHY:** Automated testing ensures that changes to the Zustand store do not introduce regressions. * **DON'T** deploy code with failing Zustand store tests. * **WHY:** Failing tests indicate potential issues with the store's logic and should be addressed before deployment. ### 3.2. Code Examples """typescript // Example: Unit testing Zustand store actions import { create } from 'zustand'; import { act } from 'react-dom/test-utils'; // Ensure you have react-dom installed import { renderHook } from '@testing-library/react-hooks'; interface MyStoreState { count: number; increment: () => void; decrement: () => void; } const useMyStore = create<MyStoreState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); describe('MyStore', () => { it('should initialize count to 0', () => { const { result } = renderHook(() => useMyStore()); expect(result.current.count).toBe(0); }); it('should increment count', () => { const { result } = renderHook(() => useMyStore()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it('should decrement count', () => { const { result } = renderHook(() => useMyStore()); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(-1); }); }); """ ### 3.3. Anti-Patterns * **Writing brittle tests that are tightly coupled to implementation details:** This makes tests difficult to maintain and prone to failure when refactoring code. * **Not testing edge cases or error conditions in Zustand store actions:** This can lead to unexpected behavior in production. ### 3.4. Monitoring and Alerting * **Set up alerts for failing tests in your CI/CD pipeline:** This allows you to quickly identify and address issues with the Zustand store. * **Track code coverage for Zustand store tests:** This provides a metric for assessing the completeness of your testing strategy. ## 4. Security Best Practices Specific to Zustand ### 4.1 Standards * **DO** sanitize any user-provided data before storing it in the Zustand store. * **WHY:** Prevents XSS attacks and other security vulnerabilities. * **DON'T** store sensitive information, such as passwords or API keys, directly in the Zustand store, especially client-side. * **WHY:** Such data can be easily accessed by malicious actors. Use secure storage mechanisms like HTTPS-only cookies or browser's secure storage or server-side sessions. * **DO** validate data being stored in the store to prevent unexpected data types or values from causing errors. * **WHY:** Prevents unexpected errors or potential security vulnerabilities. * **DON'T** expose the entire Zustand store to external libraries or components unnecessarily. * **WHY:** This can increase the attack surface of your application. Only expose the specific data or actions that are required. ### 4.2 Code Examples """typescript // Example: Sanitizing user input before storing in Zustand import { create } from 'zustand'; interface UserInputStoreState { userInput: string; updateUserInput: (input: string) => void; } //Basic Sanitization Function const sanitizeInput = (input: string) => { // Remove any HTML tags let cleanInput = input.replace(/<[^>]*>/g, ''); // Encode HTML entities cleanInput = cleanInput.replace(/[&<>"']/g, (m) => { switch (m) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return m; } }); return cleanInput; }; const useUserInputStore = create<UserInputStoreState>((set) => ({ userInput: '', updateUserInput: (input: string) => set({ userInput: sanitizeInput(input) }), // Sanitize })); """ ### 4.3 Anti-Patterns * **Trusting data received from external sources, such as APIs, without validation:** This can lead to the store being populated with malicious or incorrect data. * **Using "eval()" or similar functions to execute code stored in the Zustand store:** This can create significant security vulnerabilities. ### 4.4 Deployment and DevOps * **Implement regular security audits of your Zustand store and its interactions with other components:** This can help identify and address potential security vulnerabilities. * **Keep Zustand and its dependencies up to date with the latest security patches:** This can protect against known vulnerabilities. These guidelines provide a comprehensive set of standards for deployment, DevOps, monitoring, testing, and security best practices when using Zustand. By adhering to these standards, development teams can ensure the reliability, performance, and security of their applications while simplifying maintenance and reducing risk in production environments.
# Testing Methodologies Standards for Zustand This document outlines the coding standards for testing methodologies when using Zustand for state management in React applications. It aims to provide clear guidelines and best practices for unit, integration, and end-to-end testing, specifically tailored for Zustand's unique features and capabilities. Following these standards ensures maintainability, reliability, and performance of state management logic. ## 1. General Testing Principles ### 1.1. Test-Driven Development (TDD) * **Do This:** Embrace TDD by writing tests *before* implementing functionality. This helps to clarify requirements and ensures thorough testing from the outset. * **Don't Do This:** Neglect writing tests until after the implementation is complete. This often leads to incomplete or poorly designed tests. **Why:** TDD shifts the focus towards well-defined requirements and facilitates the construction of more testable and maintainable code. ### 1.2. Test Pyramid * **Do This:** Follow the structure of the test pyramid: more unit tests, fewer integration tests, and even fewer end-to-end tests. Unit tests should form the base, providing rapid feedback and excellent coverage of individual components and Zustand stores. * **Don't Do This:** Rely heavily on end-to-end tests at the expense of unit and integration tests. This makes debugging slower and more complex. **Why:** A balanced testing strategy ensures comprehensive coverage while optimizing for speed and maintainability. Unit tests are faster and more focused, while integration and E2E tests are more holistic but slower. ### 1.3. Test Naming Conventions * **Do This:** Use clear and descriptive test names. Use a standardized convention like "describe('ComponentName/Store Slice - Unit of Work', () => { ... });" or "it('should do something', () => { ... });". Consider BDD-style naming ("should do X when Y"). * **Don't Do This:** Use vague or ambiguous test names that don't clearly indicate the purpose of the test. Avoid names like "test1", "test2", etc. **Why:** Clear test names make it easier to understand the functionality being tested and quickly identify the cause of failures. ### 1.4. Test Isolation * **Do This:** Ensure tests are isolated from each other. Use mocking and stubbing judiciously to control dependencies and prevent tests from interfering with each other’s state. Zustand stores are particularly susceptible to state bleeding, so use "store.setState()" or a store reset function to clean up after each test. * **Don't Do This:** Allow tests to share state or rely on specific execution order. This can lead to flaky and unreliable test results. **Why:** Isolated tests are more reliable, reproducible, and easier to debug. ## 2. Unit Testing Zustand Stores ### 2.1. Store Structure and Testing Scope * **Do This:** Unit test each slice or logical unit of your Zustand store independently. This allows precise focus on individual state mutations and actions. Consider how the store is sliced and define test suites related to each slice. * **Don't Do This:** Create "god" tests that attempt to test the entire store in a single test case. This is brittle and difficult to maintain. **Why:** Focusing on individual slices promotes modularity and easier debugging. ### 2.2. Testing State Mutations * **Do This:** Verify that actions correctly modify the store's state. Use assertions to check the state before and after actions are called. * **Don't Do This:** Only focus on testing the return value of actions without validating the resulting state. **Why:** The primary responsibility of a Zustand store is to manage state effectively. Tests should validate state transitions. **Example:** """javascript import { create } from 'zustand'; import { describe , it, expect, beforeEach } from 'vitest'; // Or use Jest import { act } from 'react-dom/test-utils'; const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }) })); describe('Counter Store', () => { let store; beforeEach(() => { store = createCounterStore(); }); it('should initialize count to 0', () => { const { count } = store.getState(); expect(count).toBe(0); }); it('should increment the count', () => { act(() => { store.getState().increment(); }); const { count } = store.getState(); expect(count).toBe(1); }); it('should decrement the count', () => { act(() => { store.getState().decrement(); }); const { count } = store.getState(); expect(count).toBe(-1); }); it('should reset the count to 0', () => { act(() => { store.getState().increment(); store.getState().reset(); }); const { count } = store.getState(); expect(count).toBe(0); }); }); """ ### 2.3. Testing Actions and Side Effects * **Do This:** If actions trigger side effects (e.g., API calls), mock the dependencies to control the external environment. Use "jest.mock" (or equivalent in other testing frameworks) to mock API functions. Ensure actions correctly handle both success and error scenarios. * **Don't Do This:** Allow tests to make actual API calls, as this can lead to unpredictable results and slow test execution. **Why:** Mocking allows you to isolate the store's logic and simulate different scenarios without relying on external systems. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the API service const mockApiService = { fetchData: vi.fn() }; const createDataStore = () => create((set) => ({ data: null, loading: false, error: null, fetchData: async () => { set({ loading: true, error: null }); try { const data = await mockApiService.fetchData(); set({ data, loading: false }); } catch (error) { set({ error: error.message, loading: false }); } } })); describe('Data Store', () => { let store; beforeEach(() => { store = createDataStore(); }); it('should initialize with default values', () => { const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should set loading to true when fetching data', async () => { mockApiService.fetchData.mockResolvedValue({}); // Resolve immediately so loading changes. act(() => { // Wrap store changes. store.getState().fetchData(); }); expect(store.getState().loading).toBe(true); }); it('should fetch data successfully', async () => { const mockData = { name: 'Test Data' }; mockApiService.fetchData.mockResolvedValue(mockData); await act(async () => { await store.getState().fetchData(); // Wait for async function to complete. }); const { data, loading, error } = store.getState(); expect(data).toEqual(mockData); expect(loading).toBe(false); expect(error).toBeNull(); }); it('should handle errors when fetching data', async () => { mockApiService.fetchData.mockRejectedValue(new Error('Failed to fetch')); await act(async () => { await store.getState().fetchData(); // Await promise. }); const { data, loading, error } = store.getState(); expect(data).toBeNull(); expect(loading).toBe(false); expect(error).toBe('Failed to fetch'); }); }); """ ### 2.4. Testing Selectors * **Do This:** If the store uses selectors to derive data, write unit tests to verify that the selectors return the correct values based on different store states. * **Don't Do This:** Assume that selectors always work correctly without dedicated testing. **Why:** Selectors are an important part of the store logic, and testing them ensures that derived data is accurate. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect } from 'vitest'; import { act } from 'react-dom/test-utils'; const createItemStore = () => create((set, get) => ({ items: [{ id: 1, name: 'Item 1', price: 10 }, { id: 2, name: 'Item 2', price: 20 }], totalPrice: () => { const items = get().items; return items.reduce((sum, item) => sum + item.price, 0); }, addItem: (item) => set(state => ({items: [...state.items, item]})) })); describe('Item Store', () => { it('should calculate the total price correctly', () => { const store = createItemStore(); const totalPrice = store.getState().totalPrice(); expect(totalPrice).toBe(30); }); it('should update total price when an item is added', () => { const store = createItemStore(); const initialTotalPrice = store.getState().totalPrice(); const newItem = { id: 3, name: "New Item", price: 5 }; act(() => { store.getState().addItem(newItem); }); const newTotalPrice = store.getState().totalPrice(); expect(newTotalPrice).toBe(initialTotalPrice + newItem.price); // 30 + 5 = 35 }); }); """ ### 2.5. Testing with Time * **Do This:** When dealing with asynchronous operations and timers (e.g., debouncing, throttling, timeouts), leverage tools like "jest.useFakeTimers()". This creates controlled and predictable tests. * **Don't Do This:** Rely on real-time delays in tests, which can make tests slow and unreliable. **Why:** Fake timers allow you to fast-forward time and reliably test asynchronous interactions. **Example:** """javascript import { create } from 'zustand'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { act } from 'react-dom/test-utils'; const createDebouncedStore = () => create(set => ({ value: '', setValue: vi.fn((newValue) => { // This vi.fn allows you to see if setValue has been called set({value: newValue}) }), // Mock setValue for testing purposes debouncedSetValue: (newValue) => { debounceSetValueInternal(newValue,set); } })); let debounceSetValueInternal = (newValue,set) => { return setTimeout(() => { set({ value: newValue }); }, 300); }; describe('Debounced Store', () => { beforeEach(() => { vi.useFakeTimers(); // Enable fake timers. Don't use modern; use a basic fake timer implementation if you intend to use setTimeout. }); afterEach(() => { vi.useRealTimers(); }); it('should update the value after the debounce delay', () => { const store = createDebouncedStore(); act(() => { store.getState().debouncedSetValue('test'); }); // Fast-forward time by 300ms vi.advanceTimersByTime(300); // Use advanceTimersByTime expect(store.getState().value).toBe('test'); }); }); """ ### 2.6 Zustand-Specific Hooks and Utilities * **Do This:** When testing components that use the Zustand store directly through hooks, test the behavior of effects, selectors, and actions initiated through the hook. * **Don't Do This:** Neglect to test how your components interact with the state in the store. **Why:** Zustand's hook-based approach encourages testing state transformations and side effects that are triggered in components. ## 3. Integration Testing with React Components ### 3.1. Component Interactions with Zustand * **Do This:** Test how React components interact with the Zustand store. Use testing libraries like React Testing Library to simulate user interactions and verify that the store is updated correctly. * **Don't Do This:** Focus solely on the component's rendering output without validating its integration with the store. **Why:** Integration tests bridge the gap between unit tests and end-to-end tests, verifying that components and the store work together as expected. **Example:** """javascript import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { create } from 'zustand'; import { describe, it, expect, beforeEach } from 'vitest'; import { act } from 'react-dom/test-utils'; // Mock the Zustand store const createCounterStore = () => create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), })); function CounterComponent() { const [count, increment] = createCounterStore((state) => [state.count, state.increment]); return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> </div> ); } describe('Counter Component', () => { beforeEach(() => { createCounterStore.setState({ count: 0 }); // Reset state before each test. Important b/c store can be shared. }); it('should display the initial count', () => { render(<CounterComponent />); const countElement = screen.getByText('Count: 0'); expect(countElement).toBeInTheDocument(); }); it('should increment the count when the button is clicked', async () => { const user = userEvent.setup() render(<CounterComponent />); const incrementButton = screen.getByText('Increment'); await user.click(incrementButton); const countElement = screen.getByText('Count: 1'); expect(countElement).toBeInTheDocument(); }); }); """ ### 3.2. Mocking Zustand for Isolated Component Tests * **Do This:** For component-specific tests where you want to isolate the component and avoid relying on the real Zustand store, mock the store or its hook usage. This can be achieved using "jest.mock" to replace the store's hook with a mock implementation. * **Don't Do This:** Unnecessarily test the Zustand store's internal logic within component tests. Component tests should primarily focus on the component's behavior given a specific state. **Why:** Mocking the store allows you to control the state injected into the component and test different scenarios in isolation. This is useful when the component interacts with a complex store and you only need to test a specific aspect of its behavior. ### 3.3. Testing Component Side Effects * **Do This:** Verify that components correctly trigger store actions in response to user interactions or lifecycle events. Spy functions (e.g., "jest.spyOn") can be used to assert that actions are called with the expected parameters. * **Don't Do This:** Neglect to test side effects triggered by components, as these are often a critical part of the application's functionality. **Why:** Components are responsible for initiating state changes, and testing these interactions ensures that the store is updated correctly in response to user actions. ## 4. End-to-End (E2E) Testing ### 4.1. Realistic User Flows * **Do This:** End-to-end tests should simulate complete user flows through the application, including interactions with the Zustand store. Use tools like Cypress or Playwright to automate these tests. * **Don't Do This:** Write E2E tests that only cover basic functionality or skip important user interactions. **Why:** E2E tests provide a high level of confidence that the application works correctly in a real-world environment. ### 4.2. State Validation in E2E Tests * **Do This:** Include assertions to validate that the Zustand store is updated correctly as users interact with the application. This can be done by querying the UI for updated values that reflect the store's state. * **Don't Do This:** Rely solely on visual checks without validating the underlying state changes. **Why:** Validating the store's state in E2E tests ensures that the application's data is consistent and that user interactions have the expected effect on the state. ### 4.3. E2E Test Environment * **Do This:** Set up a dedicated test environment for E2E tests, including seed data and mock services to ensure consistent and predictable results. * **Don't Do This:** Run E2E tests against production environments, which can be unpredictable and potentially disruptive. **Why:** Dedicated test environments provide a controlled environment for E2E tests, ensuring that they are reliable and reproducible. ## 5. Common Anti-Patterns * **Over-mocking:** Avoid mocking everything. Mocking too much can lead to tests that are too far removed from reality and don't catch actual integration issues. * **Ignoring edge cases:** Ensure tests cover edge cases and boundary conditions to prevent unexpected behavior. * **Flaky tests:** Address flaky tests immediately. Flaky tests undermine confidence in the test suite and make it difficult to identify actual issues. Retries can be a workaround, but the core issue must be resolved. * **Lack of cleanup:** Always clean up after tests, especially when dealing with Zustand stores. Reset the store's state or mock dependencies to prevent tests from interfering with each other. * **Testing implementation details:** Tests should focus on the behavior of the store and components, not on the implementation details. Avoid asserting on private variables or internal function calls. ## 6. Technology-Specific Considerations * **React Testing Library:** Encourages testing from the user's perspective, focusing on what the user sees and interacts with. Avoid implementation-specific selectors. * **Vitest/Jest:** Popular JavaScript testing frameworks with excellent support for mocking, spying, and asynchronous testing. Use "jest.mock" for mocking dependencies. Vitest is generally faster for Vite-based projects; Jest is more widely known and used. * **Cypress/Playwright:** Powerful E2E testing tools that allow you to automate browser interactions and validate the application's behavior. By adhering to these coding standards, development teams can create robust and reliable Zustand-based applications with confidence, ensuring that state management logic is thoroughly tested and maintainable.
# State Management Standards for Zustand This document provides comprehensive coding standards for state management using Zustand. These standards are designed to ensure maintainable, performant, and secure applications. It is tailored for developers of all experience levels, providing guidance for code clarity and consistency. ## 1. Core Principles of Zustand State Management ### 1.1 Simplicity and Minimalism **Do This:** * Embrace Zustand's simplicity. Keep state definitions concise and business-logic-focused. Avoid over-complicating state with derived data unless absolutely necessary. **Don't Do This:** * Introduce unnecessary abstractions or layers of complexity. Zustands strength lies in direct access and mutations. Don't try to mimic Redux-style architectures. **Why:** Simplicity reduces cognitive load, enables faster development, and makes debugging easier. It also aligns with Zustands core philosophy. **Example:** """javascript import { create } from 'zustand' const useBearStore = create((set) => ({ bears: 0, increaseBears: () => set((state) => ({ bears: state.bears + 1 })), removeAllBears: () => set({ bears: 0 }), })) export default useBearStore; """ ### 1.2 Explicit Data Flow **Do This:** * Define clear data flow paths. Actions within the store should be the primary way to update the state. This promotes maintainability and traceability. **Don't Do This:** * Mutate state directly outside of the store. This breaks the unidirectional data flow, which makes debugging and reasoning about your application state difficult. **Why:** Explicit data flow improves predictability and makes it easier to track state changes. **Example:** """javascript import { create } from 'zustand' const useProductsStore = create((set) => ({ products: [], fetchProducts: async () => { const response = await fetch('/api/products'); // Simulating product API const data = await response.json(); set({ products: data }); }, addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductsStore; """ ### 1.3 Reactivity and Selectors **Do This:** * Use selectors to extract specific parts of the state that components need. Subscribing components only to the necessary parts of the state enhances performance. * Memoize selectors to avoid unnecessary re-renders. When using selectors frequently, especially with large state objects, memoization can substantially reduce render load. **Don't Do This:** * Subscribe components to the entire store state when only a small part of it is needed. * Compute values directly within components that are derived from the state. This tightly couples logic to components and makes it hard to optimize re-renders. **Why:** Selectors and memoization improve performance by reducing the amount of re-rendering in your React components. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (itemId) => set((state) => ({ items: state.items.filter(item => item.id !== itemId) })), totalItems: () => { // Derived Value, could be enhanced return useCartStore.getState().items.length; }, totalPrice: () => { // Derived Value, could be enhanced return useCartStore.getState().items.reduce((acc, item) => acc + item.price, 0) } })) // Component Using Selectors, SHALLOW comparison for re-renders! function CartSummary() { const [totalItems, totalPrice] = useCartStore( (state) => [state.totalItems, state.totalPrice], shallow ) return ( <div> Total Items: {totalItems()} Total Price: ${totalPrice()} </div> ) } export default useCartStore; """ ### 1.4 Asynchronous Actions **Do This:** * Handle asynchronous logic (e.g., API fetching) directly inside store actions. Use "async/await" or "Promises" for clarity. **Don't Do This:** * Perform asynchronous actions outside of the store and then update the state. This can lead to race conditions or inconsistent state updates. **Why:** Centralizing asynchronous operations within the store provides better control and error handling. **Example:** """javascript import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, isLoading: false, error: null, fetchUser: async (userId) => { set({ isLoading: true, error: null }); // Set loading state try { const response = await fetch("/api/users/${userId}"); const data = await response.json(); set({ user: data, isLoading: false }); // Update user data and reset loading } catch (error) { set({ error: error.message, isLoading: false }); // Set error state } }, })); export default useUserStore; """ ## 2. Zustand Store Structure and Organization ### 2.1 Modular Stores **Do This:** * Divide your application state into smaller, manageable stores based on logical domains (e.g., user, product, cart). Favor Composition over Inheritance. **Don't Do This:** * Create a single monolithic store for the entire application if your app grows complex. This will lead to performance bottlenecks and make the code hard to follow. **Why:** Modular stores improve code organization, reduce the scope of changes, and make it easier to test and reuse state logic. **Example:** """javascript // userStore.js import { create } from 'zustand' const useUserStore = create((set) => ({ user: null, setUser: (user) => set({ user }), })); export default useUserStore; // productStore.js import { create } from 'zustand' const useProductStore = create((set) => ({ products: [], addProduct: (product) => set((state) => ({ products: [...state.products, product] })), })); export default useProductStore; // cartStore.js import { create } from 'zustand' const useCartStore = create((set) => ({ items: [], addItem: (item) => set((state) => ({ items: [...state.items, item] })), })); export default useCartStore; """ ### 2.2 Group Related State **Do This:** * Logically group pieces of the store that are related. Avoid scattering related state properties across the store definition. **Don't Do This:** * Separating the "isLoading" loading flag, and error states from async actions that they're associated with. **Why:** Improve code readability and developer experience. **Example:** """javascript import { create } from 'zustand' const useAuthStore = create((set) => ({ authState: { // Grouping properties isAuthenticated: false, token: null, isLoading: false, error: null }, login: async (username, password) => { set((state) => ({ authState: { ...state.authState, isLoading: true, error: null } })); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const data = await response.json(); set((state) => ({ authState: { isAuthenticated: true, token: data.token, isLoading: false, error: null } })); } catch (error) { set((state) => ({ authState: { ...state.authState, isLoading: false, error: error.message } })); } }, logout: () => { set((state) => ({ authState: { isAuthenticated: false, token: null, isLoading: false, error: null } })); }, })); export default useAuthStore; """ ### 2.3 Using "persist" Middleware **Do This:** * Use "zustand/middleware"'s "persist" middleware when you need to persist state across browser sessions. This is especially useful for user authentication or cart data. Configuring the storage adapter when necessary. **Don't Do This:** * Persist sensitive information directly into local storage without proper encryption. * Persisting state unnecessarily without consideration for the limited local storage budget. **Why:** Persistence enhances user experience by preserving state across sessions but necessitates cautious handling of sensitive data. **Example:** """javascript import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' const useSettingsStore = create( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'settings-storage', // unique name storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used } ) ); export default useSettingsStore; """ ## 3. Zustand Action Design ### 3.1 Atomic Updates **Do This:** * Design actions to perform atomic updates to the state. Each action should represent a single, logical change to the state. **Don't Do This:** * Bunch unrelated state updates in a single action, making maintenance difficult. * Create actions that trigger cascading updates across multiple stores. This tightens coupling. **Why:** Atomic updates lead to simpler and more predictable state transitions, which are easier to test and debug. **Example:** """javascript import { create } from 'zustand' const useTaskStore = create((set) => ({ tasks: [], addTask: (task) => set((state) => ({ tasks: [...state.tasks, task] })), removeTask: (taskId) => set((state) => ({ tasks: state.tasks.filter((task) => task.id !== taskId) })), updateTaskStatus: (taskId, completed) => // Focused Update set((state) => ({ tasks: state.tasks.map((task) => task.id === taskId ? { ...task, completed } : task ), })), })); export default useTaskStore; """ ### 3.2 Naming Conventions **Do This:** * Use clear and consistent naming conventions for actions. Action names should describe what they do. A good naming system is "verbNoun": e.g. "addProduct", "removeUser" etc. **Don't Do This:** * Use vague or ambiguous action names that don't clearly indicate their purpose. **Why:** Consistent naming ensures code readability and makes it easier to understand the purpose of each action. **Example:** """javascript import { create } from 'zustand' const useCounterStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), })); export default useCounterStore; """ ### 3.3 Decoupled Logic **Do This:** * Keep store logic focused on state updates. Delegate complex business logic to separate utility functions or services. **Don't Do This:** * Embed complex computations or algorithms directly within the state update functions. **Why:** Decoupled logic makes stores easier to understand, test, and maintain. **Example:** """javascript // utils/taxCalculator.js export const calculateTax = (price, taxRate) => { return price * taxRate; }; // store.js import { create } from 'zustand' import { calculateTax } from './utils/taxCalculator'; const useProductStore = create((set) => ({ price: 100, taxRate: 0.10, setTaxRate: (rate) => set({ taxRate: rate }), calculateTaxedPrice: () => { // Moved the calculateTax logic to external utility. Very easy to test! return useProductStore.getState().price + calculateTax(useProductStore.getState().price, useProductStore.getState().taxRate); }, })); export default useProductStore; """ ## 4. Optimization and Performance ### 4.1 Shallow Equality **Do This:** * When selecting state, use "shallow" from "zustand/shallow" for shallow equality checks to prevent unnecessary re-renders. * Structure your state to encourage shallow comparisons. Avoid deeply nested objects when possible. **Don't Do This:** * Skip shallow equality checks when performance is important. * Modify nested objects within the state immutably; otherwise, shallow comparisons won't work as expected. **Why:** Shallow equality ensures that components only re-render when the relevant state actually changes, reducing unnecessary updates. **Example:** """javascript import { create } from 'zustand' import { shallow } from 'zustand/shallow' const useProfileStore = create((set) => ({ profile: { name: 'John Doe', age: 30 }, updateName: (name) => set((state) => ({ profile: { ...state.profile, name } })), updateAge: (age) => set((state) => ({ profile: { ...state.profile, age } })), })); function ProfileComponent() { const { name, age } = useProfileStore( (state) => ({ name: state.profile.name, age: state.profile.age }), shallow ); return ( <div> Name: {name}, Age: {age} </div> ); } export default useProfileStore; """ ### 4.2 Minimize State Size **Do This:** * Only store essential data in Zustand store. Avoid duplicating data or storing derived data if it can be easily computed. **Don't Do This:** * Inflate stores with large or redundant data, increasing memory usage and potentially affecting performance. **Why:** Minimize state size improves performance by reducing the overhead of state updates and re-renders. ### 4.3 Immutability **Do This:** * Ensure that all operations in the store are done immutably to trigger re-renders. * Use "..." spread operator or lodash/immer to ensure immutability **Don't Do This:** * Directly mutate the state. **Why:** Immutability ensure that new references are created and React knows to re-render the component to reflect the changes in the store. **Example:** """javascript import { create } from 'zustand' const useTodosStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map((todo) => todo.id === id ? { ...todo, completed: !todo.completed } : todo ), })), })); export default useTodosStore; """ ## 5. Testing ### 5.1 Unit Testing **Do This:** * Write unit tests for your Zustand stores to ensure that actions update the state correctly. * Test actions with different inputs and check for expected state changes. **Don't Do This:** * Skip testing Zustand stores, assuming they are simple and error-free. **Why:** Unit tests ensure that your state management logic is correct. **Example:** """javascript import { create } from 'zustand' import { act } from 'react-dom/test-utils'; import { renderHook } from '@testing-library/react'; const useTestStore = create((set) => ({ value: 0, increment: () => set((state) => ({ value: state.value + 1 })), setValue: (newValue) => set({ value: newValue }), })); describe('useTestStore', () => { beforeEach(() => { // Reset the store before each test useTestStore.setState({value: 0}, true); }); it('should initialize with a value of 0', () => { const { result } = renderHook(() => useTestStore()); expect(result.current.value).toBe(0); }); it('should increment the value by 1', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.increment(); }); expect(useTestStore.getState().value).toBe(1); }); it('should set the value to a new value', () => { const { result } = renderHook(() => useTestStore()); act(() => { result.current.setValue(10); }); expect(useTestStore.getState().value).toBe(10); }); }); export default useTestStore; """ ### 5.2 Integration Testing **Do This:** * Test how Zustand stores interact with your React components and other parts of your application. **Don't Do This:** * Ignore integration tests, as they ensure that the entire system works correctly. **Why:** Integration tests verify that your state management solution integrates seamlessly with the rest of your application. ### 5.3 Mocking Dependencies **Do This:** * Mock external dependencies like APIs in your tests to ensure that tests are fast and reliable. **Don't Do This:** * Rely on live APIs in your tests, which can make the tests slow and flaky. **Why:** Mocking dependencies isolates your components and stores, allowing you to test them in a controlled environment. ## 6. Security Considerations ### 6.1 Data Encryption **Do This:** * If you are persisting sensitive data using "zustand/middleware" persisted state, encrypt the data before storing it in local storage. **Don't Do This:** * Store sensitive data in plain text. **Why:** Encryption protects sensitive data from unauthorized access, ensuring a higher level of security for your application. ### 6.2 Input Validation **Do This:** * Validate all inputs to Zustand store actions to prevent invalid data from being stored. **Don't Do This:** * Trust user input without validation. **Why:** Input validation prevents security vulnerabilities like cross-site scripting (XSS) and SQL injection. ### 6.3 Authentication and Authorization **Do This:** * Implement proper authentication and authorization mechanisms to protect sensitive state and actions. **Don't Do This:** * Expose sensitive actions or state to unauthorized users. **Why:** Authentication and authorization ensure that only authorized users can access and modify sensitive data, protecting your application from malicious attacks. ## 7. Conclusion By adhering to these Zustand coding standards, you can create maintainable, performant, and secure state management solutions. Zustand provides a simple and powerful way to manage state in React applications, and these guidelines will help you use it effectively. Remember to prioritize simplicity, clarity, and testability in your code.
# API Integration Standards for Zustand This document outlines coding standards for integrating Zustand stores with backend services and external APIs. These standards aim to ensure maintainability, performance, and security in React applications leveraging Zustand for state management. ## 1. General Principles ### 1.1. Separation of Concerns **Standard:** Isolate API interaction logic from UI components and Zustand store definitions. **Why:** This promotes better testability, reusability, and separation of concerns. Changes to the API or the UI won't directly impact each other, reducing coupling and making the codebase easier to understand and maintain. **Do This:** Create dedicated service modules or hooks for interacting with APIs. Avoid direct API calls within components or store definitions. **Don't Do This:** Embed "fetch" or "axios" calls directly within your UI components or Zustand store's "set" function. ### 1.2. Asynchronous Actions **Standard:** Use asynchronous actions within Zustand stores to handle API requests. **Why:** API calls are inherently asynchronous. Handling them correctly prevents blocking the UI thread and ensures data consistency. **Do This:** Define asynchronous functions within your store's "create" function. Use "async/await" for cleaner syntax. **Don't Do This:** Perform synchronous API calls, which can lead to UI freezes. Avoid complex promise chaining within the "set" function of the store. ### 1.3. Error Handling **Standard:** Implement robust error handling mechanisms for API requests within Zustand stores. **Why:** API requests can fail for various reasons (network errors, server errors, invalid data). Proper error handling prevents application crashes and provides users with informative feedback. **Do This:** Wrap API calls in "try...catch" blocks. Update the Zustand store to reflect the error state. Provide meaningful error messages to the user via the UI. **Don't Do This:** Ignore errors or let them bubble up unhandled. Use generic catch-all error messages without providing useful context. ### 1.4. Loading State **Standard:** Maintain a loading state in the Zustand store to indicate when an API request is in progress. **Why:** This allows you to display loading indicators to the user, improving the user experience and preventing accidental duplicate requests. **Do This:** Include a "loading" property in your store. Set it to "true" before making the API call and "false" after the call is completed (regardless of success or failure). **Don't Do This:** Make the user guess if the app is loading/processing something. ## 2. Implementation Details ### 2.1. API Service Module **Standard:** Create a dedicated service module or file for handling API interactions. **Why:** Improves code organization, testability, and reusability of API calls. **Do This:** Create a "api/apiService.js" (or similar name) file: """javascript // api/apiService.js import axios from 'axios'; const API_BASE_URL = 'https://api.example.com'; const apiService = { async getData(id) { try { const response = await axios.get("${API_BASE_URL}/data/${id}"); return response.data; } catch (error) { console.error('Error fetching data:', error); throw error; // Re-throw to be handled in the Zustand store } }, async postData(data) { try { const response = await axios.post("${API_BASE_URL}/data", data); return response.data; } catch (error) { console.error('Error posting data:', error); throw error; // Re-throw to be handled in the Zustand store } }, // Add more API functions as needed }; export default apiService; """ **Don't Do This:** Directly use "fetch" or "axios" within the Zustand store. ### 2.2. Zustand Store Definition **Standard:** Integrate the API service within the Zustand store using asynchronous actions. **Why:** Provides a centralized and reactive way to manage API data and state. **Do This:** """javascript // store/dataStore.js import { create } from 'zustand'; import apiService from '../api/apiService'; const useDataStore = create((set, get) => ({ data: null, loading: false, error: null, fetchData: async (id) => { set({ loading: true, error: null }); // Start loading try { const data = await apiService.getData(id); set({ data, loading: false }); // Success! } catch (error) { set({ error: error.message || 'Failed to fetch data', loading: false }); // Failure! } }, postData: async (newData) => { set({ loading: true, error: null }); try { const data = await apiService.postData(newData); set({ data, loading: false }); // Potentially update a list of items if needed: // set(state => ({ items: [...state.items, data], loading: false })); } catch (error) { set({ error: error.message || 'Failed to post data', loading: false }); } }, clearData: () => { set({ data: null }); }, // Add other store actions as needed })); export default useDataStore; """ **Don't Do This:** Mutate the state directly. Access the API service outside the store in components when the store is meant to manage that data. ### 2.3. Component Usage **Standard:** Consume the Zustand store within React components to access and display API data. **Why:** Provides a reactive and efficient way to update UI based on API data changes. **Do This:** """jsx // components/DataDisplay.jsx import React, { useEffect } from 'react'; import useDataStore from '../store/dataStore'; function DataDisplay({ dataId }) { const data = useDataStore((state) => state.data); const loading = useDataStore((state) => state.loading); const error = useDataStore((state) => state.error); const fetchData = useDataStore((state) => state.fetchData); useEffect(() => { fetchData(dataId); // Fetch data when the component mounts or dataId changes }, [dataId, fetchData]); if (loading) { return <p>Loading...</p>; } if (error) { return <p>Error: {error}</p>; } if (!data) { return <p>No data available.</p>; } return ( <div> <h1>Data Display</h1> <p>ID: {data.id}</p> <p>Name: {data.name}</p> {/* Display other data properties */} </div> ); } export default DataDisplay; """ **Don't Do This:** Pass down API data as props through multiple layers of components, especially if those components don't directly need or modify the data. ### 2.4. Optimistic Updates **Standard:** Use optimistic updates where it enhances user experience with proper roll-back in case of errors. **Why:** Makes the application feel more responsive by immediately updating the UI, even before the API request completes. **Do This:** """javascript // store/dataStore.js import { create } from 'zustand'; import apiService from '../api/apiService'; const useDataStore = create((set, get) => ({ items: [], loading: false, error: null, addItem: async (newItem) => { const optimisticId = Date.now().toString(); // create temporary ID const optimisticItem = { ...newItem, id: optimisticId }; // Optimistically update the UI set(state => ({ items: [...state.items, optimisticItem] })); try { const savedItem = await apiService.postData(newItem); // Replace the temporary item with saved item from API set(state => ({ items: state.items.map(item => item.id === optimisticId ? savedItem : item ) })); } catch (error) { // If error, remove the optimistically added item and set the error state set(state => ({ items: state.items.filter(item => item.id !== optimisticId), error: error.message || 'Failed to add item', })); } finally { set({ loading: false }); } }, })); export default useDataStore; """ **Don't Do This:** Apply Optimistic updates without proper error handling and roll-back mechanism. ## 3. Advanced Patterns ### 3.1. Middleware for API Handling **Standard:** Use Zustand middleware for cross-cutting concerns related to API interactions (e.g., logging, caching, authentication). **Why:** Reduces boilerplate code and promotes reusable logic. **Do This:** Create middleware to log API calls: """javascript // middleware/apiMiddleware.js const apiMiddleware = (config) => (set, get, api) => config((args) => { console.log('API Call:', args); set(args); }, get, api); export default apiMiddleware; // store/dataStore.js import { create } from 'zustand'; import apiService from '../api/apiService'; import apiMiddleware from '../middleware/apiMiddleware'; const useDataStore = create(apiMiddleware((set, get) => ({ // existing store definition }))); """ ### 3.2. Debouncing/Throttling API Calls **Standard:** Implement debouncing or throttling for API calls triggered by frequent user input (e.g., search). **Why:** Prevents excessive API requests and reduces server load. **Do This:** Use libraries like "lodash" to debounce API calls: """javascript import { debounce } from 'lodash'; import { create } from 'zustand'; import apiService from '../api/apiService'; const useSearchStore = create((set) => ({ searchTerm: '', searchResults: [], loading: false, error: null, setSearchTerm: (term) => set({ searchTerm: term }), // Debounced search function search: debounce(async (term) => { set({ loading: true, error: null }); try { const results = await apiService.searchData(term); set({ searchResults: results, loading: false }); } catch (error) { set({ error: error.message || 'Search failed', loading: false }); } }, 300), // 300ms debounce delay })); export default useSearchStore; """ ### 3.3. Data Transformation **Standard:** Transform API data within the Zustand store before storing it. **Why:** Ensures data consistency and optimizes data for the UI. Avoid complex transformations in UI components. **Do This:** Transform the data after fetching it in the store. """javascript // store/dataStore.js import { create } from 'zustand'; import apiService from '../api/apiService'; const useDataStore = create((set) => ({ rawData: null, transformedData: null, fetchData: async (id) => { try { const rawData = await apiService.getData(id); // Transform the raw data const transformedData = { id: rawData.id, displayName: rawData.firstName + ' ' + rawData.lastName, // Add other transformations as needed }; set({ rawData, transformedData }); } catch (error) { //handle errors } }, })); export default useDataStore; """ ### 3.4. Pagination **Standard**: Implement proper pagination in Zustand when dealing with large datasets from APIs. **Why:** Avoids overwhelming the UI and improves performance by loading data in chunks. **Do This:** Integrate pagination params with api calls and manage pagination state using Zustand. """javascript import { create } from 'zustand'; import apiService from '../api/apiService'; const useProductsStore = create((set,get) => ({ products: [], currentPage: 1, productsPerPage: 20, totalPages: 1, fetchProducts: async (page) => { try { const response = await apiService.getProducts(page, get().productsPerPage); set({ products: response.data, currentPage: page, totalPages: response.totalPages, // Assume API returns total pages }); } catch (error) { // Error handling } }, goToNextPage: () => { set(state => ({ currentPage: Math.min(state.currentPage + 1, state.totalPages), })); get().fetchProducts(get().currentPage); }, goToPreviousPage: () => { set(state => ({ currentPage: Math.max(state.currentPage - 1, 1), })); get().fetchProducts(get().currentPage); }, })); export default useProductsStore; """ ## 4. Security Considerations ### 4.1. Authentication and Authorization **Standard:** Handle authentication tokens securely and protect API endpoints from unauthorized access. **Why:** Prevents unauthorized data access and potential security breaches. **Do This:** Store tokens securely (e.g., using "localStorage" with caution or "httpOnly" cookies). Include tokens in API request headers. Implement proper authorization checks on the server-side. Use environment variables and secrets management for API keys. **Don't Do This:** Store sensitive tokens in plain text in client-side code or Zustand store. ### 4.2. Data Validation **Standard:** Validate data received from the API and before sending data to the API. **Why:** Prevents data corruption and potential security vulnerabilities (e.g., cross-site scripting). **Do This:** Use libraries like "yup" or "zod" to validate data schemas. Sanitize user inputs before sending them to the API. ## 5. Performance Optimization ### 5.1. Selectors **Standard:** Use selectors with the "useStore" hook to select only the necessary parts of the Zustand state. **Why:** Prevents unnecessary re-renders of components when only a small part of the state changes. **Do This:** Select specific fields within components: """jsx // components/DataDisplay.jsx import React from 'react'; import useDataStore from '../store/dataStore'; function DataDisplay() { const name = useDataStore(state => state.data?.name); // Select only the name const id = useDataStore(state => state.data?.id); // Select only the ID return ( <div> <h1>Name: {name}</h1> <p>ID: {id}</p> </div> ); } export default DataDisplay; """ ### 5.2. Memoization **Standard:** Memoize derived data using "useMemo" or similar techniques. **Why:** Prevents unnecessary recalculations of derived data when the input data hasn't changed. ### 5.3. Avoiding Unnecessary Re-renders **Standard:** Be mindful of object identity especially when setting state in Zustand. **Why:** Zustand uses shallow equality checks to determine when to re-render components subscribing to the store. Returning a new object (even if properties are the same) will trigger a re-render. **Do This:** Mutate objects where possible, or only return a *new* object when state has actually changed. The "immer" middleware can automatically handle this. """javascript import { create } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; const useDataStore = create(immer((set) => ({ items: [], addItem: (item) => set(state => { state.items.push(item); // Immer makes this safe and efficient }) }))); """ ## 6. Testing ### 6.1. Unit Testing API Interactions **Standard:** Mock API calls in unit tests to isolate Zustand store logic. **Why:** Ensures that the Zustand store functions correctly regardless of the API's availability or behavior. **Do This:** Use libraries like "jest" and "msw" (Mock Service Worker) to mock API calls. Test the store's state updates, error handling, and loading state. ## 7. Code Formatting and Linting **Standard:** Use a consistent code style and linting rules. **Why:** Improves code readability and maintainability **Do This:** Use Prettier for code formatting and ESLint for linting. Setup pre-commit hooks to automatically format and lint code before committing. ## 8. Documentation **Standard:** Document APIs **Why:** Aids in understanding and managing APIs **Do this:** Use OpenAPI to document standard APIs. By adhering to these coding standards, development teams can build robust, maintainable, and secure React applications that leverage the power of Zustand for state management and API integration. These are living guidelines that should be revised as Zustand evolves.