projectrules.ai

Zustand Best Practices

zustandstate-managementreactbest-practicesperformance-optimization

Description

This rule provides guidelines for using Zustand, a simple and unopinionated state management library, in React applications. It covers best practices for code organization, performance optimization, testing, and common pitfalls to avoid.

Globs

**/*.{js,jsx,ts,tsx}
---
description: This rule provides guidelines for using Zustand, a simple and unopinionated state management library, in React applications. It covers best practices for code organization, performance optimization, testing, and common pitfalls to avoid.
globs: **/*.{js,jsx,ts,tsx}
---

# Zustand Best Practices

This document outlines best practices for using Zustand in your React applications. Zustand is a simple and unopinionated state management library. Following these guidelines will help you write maintainable, performant, and scalable applications.

## 1. Code Organization and Structure

### 1.1. Directory Structure

Organize your store files in a dedicated directory, such as `store` or `state`, at the root of your project or within a specific feature directory. This enhances discoverability and maintainability.


src/
├── components/
│   ├── ...
├── store/
│   ├── index.ts          # Main store file (optional)
│   ├── bearStore.ts      # Example store
│   ├── fishStore.ts      # Example store
│   └── utils.ts         # Utility functions for stores
├── App.tsx
└── ...


### 1.2. File Naming Conventions

Use descriptive names for your store files, typically reflecting the domain or feature the store manages. For example, `userStore.ts`, `cartStore.js`, or `settingsStore.tsx`.  Use PascalCase for the store name itself (e.g., `UserStore`).

### 1.3. Module Organization

- **Single Store per File:**  Prefer defining one Zustand store per file.  This improves readability and maintainability.
- **Slices Pattern:** For complex stores, consider using the slices pattern to divide the store into smaller, more manageable pieces.  Each slice manages a specific part of the state and its related actions.

typescript
// store/bearStore.ts
import { StateCreator, create } from 'zustand';

interface BearSlice {
  bears: number;
  addBear: () => void;
}

const createBearSlice: StateCreator<BearSlice> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
});

export const useBearStore = create<BearSlice>()((...a) => ({
  ...createBearSlice(...a),
}));

// Another slice could be in fishStore.ts, etc.


### 1.4. Component Architecture

- **Presentational and Container Components:**  Separate presentational (UI) components from container components that interact with the Zustand store. Container components fetch data from the store and pass it down to presentational components.
- **Hooks for Data Fetching:** Utilize Zustand's `useStore` hook within container components to subscribe to specific parts of the state.

### 1.5. Code Splitting Strategies

- **Lazy Loading Stores:**  Load stores on demand using dynamic imports.  This can reduce the initial bundle size, especially for larger applications.
- **Feature-Based Splitting:** Split your application into feature modules and create separate stores for each feature.  This allows for independent loading and reduces dependencies between different parts of the application.

## 2. Common Patterns and Anti-patterns

### 2.1. Design Patterns Specific to Zustand

- **Colocated Actions and State:**  Keep actions and the state they modify within the same store. This promotes encapsulation and makes it easier to understand how the store's state is updated.
- **Selectors:** Use selectors to derive computed values from the store's state. Selectors should be memoized to prevent unnecessary re-renders.

typescript
// store/userStore.ts
import { create } from 'zustand';

interface UserState {
  name: string;
  age: number;
}

interface UserActions {
  setName: (name: string) => void;
  isAdult: () => boolean; // Selector
}

export const useUserStore = create<UserState & UserActions>((set, get) => ({
  name: 'John Doe',
  age: 20,
  setName: (name) => set({ name }),
  isAdult: () => get().age >= 18, // Selector
}));


### 2.2. Recommended Approaches for Common Tasks

- **Asynchronous Actions:** Use `async/await` within actions to handle asynchronous operations such as fetching data from an API.

typescript
interface DataState {
  data: any | null;
  isLoading: boolean;
  fetchData: () => Promise<void>;
}

export const useDataStore = create<DataState>((set) => ({
  data: null,
  isLoading: false,
  fetchData: async () => {
    set({ isLoading: true });
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      set({ data, isLoading: false });
    } catch (error) {
      console.error('Error fetching data:', error);
      set({ isLoading: false, data: null });
    }
  },
}));


- **Persisting State:** Use the `zustand/middleware`'s `persist` middleware to persist the store's state to local storage or another storage mechanism.  Configure a `partialize` function to select the state you want to persist.

typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface AuthState {
  token: string | null;
  setToken: (token: string | null) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      token: null,
      setToken: (token) => set({ token }),
    }),
    {
      name: 'auth-storage', // unique name
      partialize: (state) => ({ token: state.token }), // Only persist the token
    }
  )
)


### 2.3. Anti-patterns and Code Smells to Avoid

- **Over-reliance on Global State:** Avoid storing everything in a single global store.  Instead, break down your application's state into smaller, more manageable stores based on feature or domain.
- **Mutating State Directly:** Never mutate the state directly. Always use the `set` function provided by Zustand to ensure proper state updates and re-renders.  Consider using Immer middleware (`zustand/middleware`) for immutable updates with mutable syntax.
- **Complex Selectors without Memoization:**  Avoid complex selectors that perform expensive computations without memoization.  This can lead to performance issues, especially with frequent state updates.
- **Creating Stores Inside Components:**  Do not define Zustand stores inside React components.  This will cause the store to be re-created on every render, leading to data loss and unexpected behavior.

### 2.4. State Management Best Practices

- **Single Source of Truth:**  Treat the Zustand store as the single source of truth for your application's state.
- **Minimize State:** Store only the essential data in the store.  Derive computed values using selectors.
- **Clear State Transitions:**  Ensure that state transitions are predictable and well-defined.  Avoid complex and convoluted update logic.

### 2.5. Error Handling Patterns

- **Try/Catch Blocks in Actions:**  Wrap asynchronous actions in `try/catch` blocks to handle potential errors.  Update the state to reflect the error status.
- **Error Boundary Components:**  Use React error boundary components to catch errors that occur during rendering.
- **Centralized Error Logging:** Implement a centralized error logging mechanism to track errors that occur in your application.  Send error reports to a monitoring service.

## 3. Performance Considerations

### 3.1. Optimization Techniques

- **Selective Updates:**  Use selectors to subscribe to only the specific parts of the state that your component needs. This prevents unnecessary re-renders when other parts of the state change.
- **Shallow Equality Checks:** By default, Zustand uses strict equality (`===`) for state comparisons. If you're using complex objects, use `shallow` from `zustand/shallow` as an equality function in `useStore` to avoid unnecessary re-renders.
- **Memoization:** Memoize selectors using libraries like `reselect` or `lodash.memoize` to prevent unnecessary re-computations.
- **Batch Updates:** Use `unstable_batchedUpdates` from `react-dom` to batch multiple state updates into a single render cycle.

### 3.2. Memory Management

- **Avoid Leaks:** Ensure that you are not creating memory leaks by properly cleaning up any subscriptions or event listeners that you create.
- **Limit Store Size:** Keep your stores as small as possible by only storing the data that you need.

### 3.3. Rendering Optimization

- **Virtualization:** If you are rendering large lists of data, use virtualization techniques (e.g., `react-window`, `react-virtualized`) to only render the visible items.
- **Code Splitting:**  Split your application into smaller bundles to reduce the initial load time.

### 3.4. Bundle Size Optimization

- **Tree Shaking:** Ensure that your build process is configured to perform tree shaking, which removes unused code from your bundles.
- **Minimize Dependencies:**  Use only the dependencies that you need.  Avoid importing entire libraries when you only need a few functions.
- **Compression:**  Compress your bundles using tools like Gzip or Brotli.

### 3.5. Lazy Loading Strategies

- **Component-Level Lazy Loading:**  Lazy load components using `React.lazy` and `Suspense`.
- **Route-Based Lazy Loading:**  Lazy load routes using `react-router-dom`'s `lazy` function.

## 4. Security Best Practices

### 4.1. Common Vulnerabilities and How to Prevent Them

- **Cross-Site Scripting (XSS):** Sanitize user inputs to prevent XSS attacks.
- **Cross-Site Request Forgery (CSRF):** Protect your API endpoints from CSRF attacks by implementing CSRF tokens.
- **Sensitive Data Exposure:** Avoid storing sensitive data in the store unless absolutely necessary. Encrypt sensitive data if it must be stored.

### 4.2. Input Validation

- **Server-Side Validation:** Always validate user inputs on the server-side.
- **Client-Side Validation:** Perform client-side validation to provide immediate feedback to the user.

### 4.3. Authentication and Authorization Patterns

- **JSON Web Tokens (JWT):** Use JWTs for authentication and authorization.
- **Role-Based Access Control (RBAC):** Implement RBAC to control access to different parts of your application based on user roles.

### 4.4. Data Protection Strategies

- **Encryption:** Encrypt sensitive data at rest and in transit.
- **Data Masking:** Mask sensitive data in logs and other outputs.

### 4.5. Secure API Communication

- **HTTPS:** Use HTTPS to encrypt communication between the client and server.
- **API Rate Limiting:** Implement API rate limiting to prevent abuse.

## 5. Testing Approaches

### 5.1. Unit Testing Strategies

- **Test Store Logic:** Write unit tests to verify the logic of your Zustand stores.
- **Mock Dependencies:** Mock any external dependencies (e.g., API calls) to isolate the store logic.
- **Test State Transitions:** Ensure that state transitions are correct by asserting the expected state after each action.

### 5.2. Integration Testing

- **Test Component Interactions:** Write integration tests to verify that your components interact correctly with the Zustand store.
- **Render Components:** Render your components and simulate user interactions to test the integration between the UI and the store.

### 5.3. End-to-End Testing

- **Simulate User Flows:** Write end-to-end tests to simulate complete user flows through your application.
- **Test Real-World Scenarios:** Test real-world scenarios to ensure that your application is working correctly in a production-like environment.

### 5.4. Test Organization

- **Colocate Tests:** Colocate your tests with the code that they are testing.
- **Use Descriptive Names:** Use descriptive names for your test files and test cases.

### 5.5. Mocking and Stubbing

- **Jest Mocks:** Use Jest's mocking capabilities to mock external dependencies.
- **Sinon Stubs:** Use Sinon to create stubs for functions and methods.

## 6. Common Pitfalls and Gotchas

### 6.1. Frequent Mistakes Developers Make

- **Incorrectly Using `set`:** Forgetting to use the functional form of `set` when updating state based on the previous state.
- **Not Handling Asynchronous Errors:** Failing to handle errors in asynchronous actions.
- **Over-relying on `shallow`:**  Using `shallow` equality when a deep comparison is actually required.

### 6.2. Edge Cases to Be Aware Of

- **Race Conditions:** Be aware of potential race conditions when handling asynchronous actions.
- **Context Loss:** Ensure that the Zustand provider is properly configured to avoid context loss issues.

### 6.3. Version-Specific Issues

- **Check Release Notes:**  Always check the release notes for new versions of Zustand to be aware of any breaking changes or new features.

### 6.4. Compatibility Concerns

- **React Version:**  Ensure that your version of React is compatible with the version of Zustand that you are using.

### 6.5. Debugging Strategies

- **Zustand Devtools:** Use the Zustand Devtools extension to inspect the store's state and track state changes.
- **Console Logging:**  Use console logging to debug your store logic and component interactions.
- **Breakpoints:** Set breakpoints in your code to step through the execution and inspect the state.

## 7. Tooling and Environment

### 7.1. Recommended Development Tools

- **VS Code:** Use VS Code as your code editor.
- **ESLint:**  Use ESLint to enforce code style and prevent errors.
- **Prettier:** Use Prettier to automatically format your code.
- **Zustand Devtools:**  Use the Zustand Devtools extension to inspect the store's state.

### 7.2. Build Configuration

- **Webpack:** Use Webpack to bundle your code.
- **Parcel:** Use Parcel for zero-configuration bundling.
- **Rollup:** Use Rollup for building libraries.

### 7.3. Linting and Formatting

- **ESLint:** Configure ESLint to enforce code style and best practices.
- **Prettier:** Configure Prettier to automatically format your code.
- **Husky:** Use Husky to run linters and formatters before committing code.

### 7.4. Deployment Best Practices

- **Environment Variables:** Use environment variables to configure your application for different environments.
- **CDN:** Use a CDN to serve your static assets.
- **Caching:** Implement caching to improve performance.

### 7.5. CI/CD Integration

- **GitHub Actions:** Use GitHub Actions to automate your build, test, and deployment processes.
- **CircleCI:** Use CircleCI to automate your build, test, and deployment processes.
- **Jenkins:** Use Jenkins to automate your build, test, and deployment processes.

## 8. Zustand-X (zustandx.udecode.dev)

Zustand-X builds on top of Zustand to reduce boilerplate and enhance features, providing a store factory with derived selectors and actions.

### 8.1. Key Features

- **Less Boilerplate:** Simplified store creation.
- **Modular State Management:** Derived selectors and actions.
- **Middleware Support:** Integrates with `immer`, `devtools`, and `persist`.
- **TypeScript Support:** Full TypeScript support.
- **React-Tracked Support:** Integration with `react-tracked`.

### 8.2. Usage

javascript
import { createStore } from 'zustand-x';

const repoStore = createStore('repo')({
  name: 'zustandX',
  stars: 0,
  owner: {
    name: 'someone',
    email: 'someone@xxx.com',
  },
});

// Hook store
repoStore.useStore;

// Vanilla store
repoStore.store;

// Selectors
repoStore.use.name();
repoStore.get.name();

// Actions
repoStore.set.name('new name');

// Extend selectors
repoStore.extendSelectors((state, get, api) => ({
  validName: () => get.name().trim(),
  title: (prefix) => `${prefix + get.validName()} with ${get.stars()} stars`,
}));

// Extend actions
repoStore.extendActions((set, get, api) => ({
  validName: (name) => {
    set.name(name.trim());
  },
  reset: (name) => {
    set.validName(name);
    set.stars(0);
  },
}));


### 8.3. Global Store

Combine multiple stores into a global store for easier access.

javascript
import { mapValuesKey } from 'zustand-x';

export const rootStore = {
  repo: repoStore,
  // other stores
};

export const useStore = () => mapValuesKey('use', rootStore);
export const useTrackedStore = () => mapValuesKey('useTracked', rootStore);
export const store = mapValuesKey('get', rootStore);
export const actions = mapValuesKey('set', rootStore);

// Usage
useStore().repo.name();
actions.repo.stars(store.repo.stars + 1);


## 9. zustand-ards

A library of simple, opinionated utilities for Zustand to improve the developer experience.

### 9.1 Installation

bash
pnpm i zustand-ards
# or
npm i zustand-ards


### 9.2 Basic Usage

javascript
import { withZustandards } from 'zustand-ards';

const useWithZustandards = withZustandards(useStore);
const { bears, increaseBears } = useWithZustandards(['bears', 'increaseBears']);


### 9.3 Store Hook Enhancements

- **`withZustandards`:** Combines `withArraySelector` and `withDefaultShallow`.
- **`withArraySelector`:** Adds an array selector to the store hook, eliminating the need for multiple hooks or complex selector functions.
- **`withDefaultShallow`:** Makes the store hook shallow by default, equivalent to passing `shallow` from `zustand/shallow` to the original hook.

javascript
import { withArraySelector } from 'zustand-ards';

const useStoreWithArray = withArraySelector(useExampleStore);
const { bears, increaseBears } = useStoreWithArray(['bears', 'increaseBears']);

import { withDefaultShallow } from 'zustand-ards';

const useShallowStore = withDefaultShallow(useExampleStore);
const { wizards } = useShallowStore((state) => ({ wizards: state.wizards }));


## 10. TypeScript Guide

### 10.1. Basic Usage

Annotate the store's state type using `create<T>()(...)`.

typescript
import { create } from 'zustand';

interface BearState {
  bears: number;
  increase: (by: number) => void;
}

const useBearStore = create<BearState>()((set) => ({
  bears: 0,
  increase: (by) => set((state) => ({ bears: state.bears + by }))
}));


### 10.2. Using `combine`

`combine` infers the state, so no need to type it.

typescript
import { create } from 'zustand';
import { combine } from 'zustand/middleware';

const useBearStore = create(
  combine({ bears: 0 }, (set) => ({
    increase: (by: number) => set((state) => ({ bears: state.bears + by }))
  }))
);


### 10.3. Using Middlewares

Use middlewares immediately inside `create` to ensure contextual inference works correctly. Devtools should be used last to prevent other middlewares from mutating the `setState` before it.

typescript
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

interface BearState {
  bears: number;
  increase: (by: number) => void;
}

const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by }))
      }),
      { name: 'bearStore' }
    )
  )
);


### 10.4. Authoring Middlewares and Advanced Usage

Zustand middlewares can mutate the store.  Higher-kinded mutators are available for complex type problems.

### 10.5. Middleware Examples

**Middleware that doesn't change the store type:**

typescript
import { create, State, StateCreator, StoreMutatorIdentifier } from 'zustand';

type Logger = <
  T extends State,
  Mps extends [StoreMutatorIdentifier, unknown][] = [],
  Mcs extends [StoreMutatorIdentifier, unknown][] = [],
>(f: StateCreator<T, Mps, Mcs>, name?: string) => StateCreator<T, Mps, Mcs>;

const loggerImpl = (f, name) => (set, get, store) => {
  const loggedSet = (...a) => {
    set(...a);
    console.log(...(name ? [`${name}:`] : []), get());
  };
  const setState = store.setState;
  store.setState = (...a) => {
    setState(...a);
    console.log(...(name ? [`${name}:`] : []), store.getState());
  };
  return f(loggedSet, get, store);
};

export const logger = loggerImpl;


**Middleware that changes the store type:** Requires advanced TypeScript features.

### 10.6. Slices Pattern

The slices pattern is a way to split a store into smaller, more manageable parts.

typescript
import { create, StateCreator } from 'zustand';

interface BearSlice {
  bears: number;
  addBear: () => void;
  eatFish: () => void;
}

interface FishSlice {
  fishes: number;
  addFish: () => void;
}

const createBearSlice: StateCreator<BearSlice> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({fishes: state.fishes -1}))
});

const createFishSlice: StateCreator<FishSlice> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 }))
});


export const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}));


### 10.7. Bounded useStore Hook for Vanilla Stores

Provides type safety when using `useStore` with vanilla stores.

## 11. Zustand Tools (zustand-tools)

Tools for simpler Zustand usage with React.

### 11.1. `createSimple(initStore, middlewares)`

Creates a simple store with correct typings and hooks for easier usage.

javascript
import { createSimple } from 'zustand-tools';

const demoStore = createSimple({ foo: 'bar' });

/*
 * will provide:
 * demoStore.useStore.getState().foo
 * demoStore.useStore.getState().setFoo(value)
 * demoStore.hooks.useFoo() => [value, setter] // like useState
 */

const useFoo = demoStore.hooks.useFoo;

function App() {
  const [foo, setFoo] = useFoo();

  useEffect(() => {
    setFoo('newBar');
  }, [setFoo]);

  return <div>{foo}</div>;
}


### 11.2. `createSimpleContext(initStore, middlewares)`

Basically the same as `createSimple` but returns a provider to use the store only for a specific context.

### 11.3. `useAllData()`

Returns all data from the store using a shallow compare.

### 11.4. Adding Additional Actions

Add additional actions to the generated store.

javascript
import { createSimple } from 'zustand-tools';

const { useStore } = createSimple(
  { foo: 1 },
  {
    actions: (set) => ({
      increaseFoo: (amount) => set((state) => ({ foo: state.foo + amount }))
    })
  }
);

useStore.getState().increaseFoo(5);


### 11.5. Adding Middlewares

Middlewares can be added by passing an array as middlewares in the second parameter.

javascript
import { createSimple } from 'zustand-tools';
import { devtools } from 'zustand/middleware';

const demoStore = createSimple({ foo: 'bar' }, { middlewares: [(initializer) => devtools(initializer, { enabled: true })] });


## 12. Practice with no Store Actions

### 12.1. Colocated Actions and State

The recommended usage is to colocate actions and states within the store.

javascript
export const useBoundStore = create((set) => ({
  count: 0,
  text: 'hello',
  inc: () => set((state) => ({ count: state.count + 1 })),
  setText: (text) => set({ text }),
}));


### 12.2. External Actions

An alternative approach is to define actions at the module level, external to the store.

javascript
export const useBoundStore = create(() => ({
  count: 0,
  text: 'hello',
}));

export const inc = () =>
  useBoundStore.setState((state) => ({ count: state.count + 1 }));
export const setText = (text) => useBoundStore.setState({ text });


## Conclusion

By following these best practices, you can build robust and scalable applications using Zustand. Remember to adapt these guidelines to your specific project needs and coding style.