Zustand - 极简状态管理

38 阅读5分钟

Zustand(德语意为"状态")是一个小巧、快速、可扩展的 React 状态管理解决方案。

核心哲学

  • 极简 API:只需一个 hook 调用
  • 无样板代码:无需 Provider 包裹
  • 不可变更新:类似 Redux 但更简单
  • 类型安全:一流的 TypeScript 支持

安装

npm install zustand
# 或
pnpm add zustand

基础用法

1. 创建 Store

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

// 基础 store
const useBearStore = create((set, get) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  reset: () => set({ bears: 0 }),
  
  // 异步 action
  fetchBears: async () => {
    const response = await fetch('/api/bears');
    const { data } = await response.json();
    set({ bears: data });
  },
  
  // 在 action 中访问当前状态
  increaseBy: (amount) => {
    const currentBears = get().bears;
    set({ bears: currentBears + amount });
  },
}));

// 带 TypeScript 的类型定义
interface BearStore {
  bears: number;
  fish: number;
  increasePopulation: () => void;
  eatFish: () => void;
  addBears: (amount: number) => void;
}

const useBearStore = create<BearStore>((set, get) => ({
  bears: 0,
  fish: 10,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fish: state.fish - 1 })),
  addBears: (amount) => set((state) => ({ bears: state.bears + amount })),
}));

2. 在组件中使用

function BearCounter() {
  // 获取整个 store
  const { bears, increasePopulation, removeAllBears } = useBearStore();
  
  return (
    <div>
      <h1>{bears} bears around here</h1>
      <button onClick={increasePopulation}>Add bear</button>
      <button onClick={removeAllBears}>Remove all bears</button>
    </div>
  );
}

// 性能优化:选择特定状态
function BearCounterOptimized() {
  // 只订阅 bears 的变化
  const bears = useBearStore((state) => state.bears);
  const increasePopulation = useBearStore((state) => state.increasePopulation);
  
  return (
    <div>
      <h1>{bears} bears</h1>
      <button onClick={increasePopulation}>Add bear</button>
    </div>
  );
}

// 批量选择多个状态
function BearCounterMulti() {
  const { bears, fish, increasePopulation, eatFish } = useBearStore(
    (state) => ({
      bears: state.bears,
      fish: state.fish,
      increasePopulation: state.increasePopulation,
      eatFish: state.eatFish,
    }),
    // 自定义相等性检查(可选)
    (oldState, newState) => 
      oldState.bears === newState.bears && 
      oldState.fish === newState.fish
  );
  
  return (
    <div>
      <p>Bears: {bears}</p>
      <p>Fish: {fish}</p>
      <button onClick={increasePopulation}>Add Bear</button>
      <button onClick={eatFish}>Eat Fish</button>
    </div>
  );
}

高级特性

1. 中间件系统

import { create } from 'zustand';
import { 
  devtools,      // Redux DevTools 集成
  persist,       // 持久化
  subscribeWithSelector, // 选择器订阅
  combine,       // 组合多个 store
  redux,         // Redux 模式
} from 'zustand/middleware';

// DevTools 集成
const useStore = create(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    { name: 'MyStore', enabled: process.env.NODE_ENV !== 'production' }
  )
);

// 持久化存储(localStorage/sessionStorage)
const usePersistedStore = create(
  persist(
    (set, get) => ({
      user: null,
      token: '',
      setUser: (userData) => set({ user: userData }),
      clearUser: () => set({ user: null, token: '' }),
    }),
    // 第二个参数:持久化配置(写法一)
    {
      name: 'user-storage', // 存储键名
      // 部分持久化
      partialize: (state) => ({ 
        user: state.user,
        // token 不持久化
      }),
    }
    /**
    第二个参数:持久化配置(写法二)
    {
      name: 'counter-storage', // 存储的 key(必选,用于 localStorage 标识)
      // 默认存储到 localStorage,可指定为 sessionStorage:
      // storage: createJSONStorage(() => sessionStorage)
    }
    */
  )
);

// 订阅特定状态变化
const useStore = create(
  subscribeWithSelector((set, get) => ({
    count: 0,
    text: 'hello',
    increment: () => set((state) => ({ count: state.count + 1 })),
  }))
);

// 在组件外订阅
const unsubscribe = useStore.subscribe(
  (state) => state.count,
  (count, prevCount) => {
    console.log(`Count changed from ${prevCount} to ${count}`);
  },
  {
    equalityFn: (a, b) => a === b, // 默认
    fireImmediately: true, // 立即触发
  }
);

2. Immer 集成(可变更新)

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        // 可以直接修改状态,Immer 会处理不可变更新
        state.todos.push({ 
          id: Date.now(), 
          text, 
          completed: false 
        });
      }),
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((t) => t.id === id);
        if (todo) todo.completed = !todo.completed;
      }),
  }))
);

3. Slice 模式(模块化)

// slices/counterSlice.ts
export interface CounterSlice {
  count: number;
  increment: () => void;
  decrement: () => void;
  reset: () => void;
}

const createCounterSlice = (set, get): CounterSlice => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
});

// slices/userSlice.ts
export interface UserSlice {
  user: User | null;
  token: string;
  login: (credentials: LoginCredentials) => Promise<void>;
  logout: () => void;
}

const createUserSlice = (set, get): UserSlice => ({
  user: null,
  token: '',
  login: async (credentials) => {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    });
    const data = await response.json();
    set({ user: data.user, token: data.token });
  },
  logout: () => set({ user: null, token: '' }),
});

// 组合 slices
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useStore = create(
  devtools((set, get) => ({
    ...createCounterSlice(set, get),
    ...createUserSlice(set, get),
  }))
);

4. 计算属性/派生状态

const useStore = create((set, get) => ({
  items: [],
  filter: 'all',
  
  // 直接在 store 中定义计算属性
  get filteredItems() {
    const { items, filter } = get();
    switch (filter) {
      case 'completed':
        return items.filter(item => item.completed);
      case 'active':
        return items.filter(item => !item.completed);
      default:
        return items;
    }
  },
  
  // 或者使用 getter 函数
  getCompletedItems: () => {
    const items = get().items;
    return items.filter(item => item.completed);
  },
  
  // 基于现有状态的 action
  completeAll: () => {
    const items = get().items;
    set({ items: items.map(item => ({ ...item, completed: true })) });
  },
}));

实用模式和最佳实践

1. 类型安全的 Store 工厂

// store/types.ts
export interface StoreState {
  // 状态
  bears: number;
  fish: number;
  user: User | null;
  
  // Actions
  incrementBear: () => void;
  decrementBear: () => void;
  setUser: (user: User | null) => void;
  fetchUser: (userId: string) => Promise<void>;
}

// store/createStore.ts
import { StateCreator } from 'zustand';

export const createBearSlice: StateCreator<
  StoreState,
  [['zustand/devtools', never]],
  [],
  Pick<StoreState, 'bears' | 'fish' | 'incrementBear' | 'decrementBear'>
> = (set) => ({
  bears: 0,
  fish: 10,
  incrementBear: () => set((state) => ({ bears: state.bears + 1 })),
  decrementBear: () => set((state) => ({ bears: state.bears - 1 })),
});

export const createUserSlice: StateCreator<
  StoreState,
  [['zustand/devtools', never]],
  [],
  Pick<StoreState, 'user' | 'setUser' | 'fetchUser'>
> = (set, get) => ({
  user: null,
  setUser: (user) => set({ user }),
  fetchUser: async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    set({ user });
  },
});

// store/index.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

export const useStore = create<StoreState>()(
  devtools(
    (...a) => ({
      ...createBearSlice(...a),
      ...createUserSlice(...a),
    }),
    { name: 'AppStore' }
  )
);

2. 异步操作模式

const useAsyncStore = create((set, get) => ({
  data: null,
  loading: false,
  error: null,
  
  fetchData: async (id) => {
    // 开始加载
    set({ loading: true, error: null });
    
    try {
      const response = await fetch(`/api/data/${id}`);
      if (!response.ok) throw new Error('Network error');
      
      const data = await response.json();
      set({ data, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
  
  // 带取消的异步操作
  fetchDataWithCancel: async (id) => {
    const controller = new AbortController();
    
    set({ loading: true, error: null });
    
    try {
      const response = await fetch(`/api/data/${id}`, {
        signal: controller.signal,
      });
      const data = await response.json();
      set({ data, loading: false });
    } catch (error) {
      if (error.name !== 'AbortError') {
        set({ error: error.message, loading: false });
      }
    }
    
    // 返回取消函数
    return () => controller.abort();
  },
}));

3. 表单处理

const useFormStore = create((set) => ({
  formData: {
    username: '',
    email: '',
    password: '',
  },
  errors: {},
  touched: {},
  
  // 更新单个字段
  setField: (field, value) =>
    set((state) => ({
      formData: { ...state.formData, [field]: value },
      // 标记为已触摸
      touched: { ...state.touched, [field]: true },
    })),
  
  // 批量更新
  setFormData: (updates) =>
    set((state) => ({
      formData: { ...state.formData, ...updates },
    })),
  
  // 验证
  validate: () => {
    const { formData } = get();
    const errors = {};
    
    if (!formData.email.includes('@')) {
      errors.email = 'Invalid email';
    }
    
    set({ errors });
    return Object.keys(errors).length === 0;
  },
  
  // 提交
  submit: async () => {
    if (!validate()) return false;
    
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(get().formData),
      });
      
      return response.ok;
    } catch (error) {
      set({ errors: { submit: error.message } });
      return false;
    }
  },
  
  // 重置
  reset: () => set({ 
    formData: { username: '', email: '', password: '' },
    errors: {},
    touched: {},
  }),
}));

4. 性能优化

// 1. 使用选择器避免不必要的重渲染
function ExpensiveComponent() {
  // ❌ 错误:订阅整个 store
  const store = useStore();
  
  // ✅ 正确:只订阅需要的部分
  const value = useStore((state) => state.expensiveValue);
  
  // ✅ 使用 shallow 比较
  const { prop1, prop2 } = useStore(
    (state) => ({ 
      prop1: state.prop1, 
      prop2: state.prop2 
    }),
    shallow // 来自 zustand/shallow
  );
}

// 2. 防抖更新
const useDebouncedStore = create((set) => ({
  searchQuery: '',
  _setSearchQuery: (query) => set({ searchQuery: query }),
}));

// 在组件中使用防抖
function SearchInput() {
  const setSearchQuery = useDebouncedStore((state) => state._setSearchQuery);
  const [localQuery, setLocalQuery] = useState('');
  
  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setSearchQuery(localQuery);
    }, 300);
    
    return () => clearTimeout(timeoutId);
  }, [localQuery, setSearchQuery]);
  
  return <input value={localQuery} onChange={(e) => setLocalQuery(e.target.value)} />;
}

// 3. 批量更新
const useBatchStore = create((set) => ({
  a: 0,
  b: 0,
  c: 0,
  
  // 批量更新多个状态
  updateAll: (values) => set((state) => ({
    a: values.a ?? state.a,
    b: values.b ?? state.b,
    c: values.c ?? state.c,
  })),
}));

5. 测试策略

// store/__tests__/bearStore.test.js
import { act } from 'react-test-renderer';
import { create } from 'zustand';

// 测试 store
const useTestStore = create((set) => ({
  bears: 0,
  increment: () => set((state) => ({ bears: state.bears + 1 })),
}));

describe('Bear Store', () => {
  it('should increment bears', () => {
    const { getState, setState } = useTestStore;
    
    expect(getState().bears).toBe(0);
    
    act(() => {
      getState().increment();
    });
    
    expect(getState().bears).toBe(1);
  });
  
  it('should handle initial state', () => {
    const useCustomStore = create(() => ({
      bears: 10,
    }));
    
    expect(useCustomStore.getState().bears).toBe(10);
  });
});

// 使用测试工具
import { create as actualCreate } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';

// Mock store 用于测试
const createMockStore = (initialState) => {
  let store = null;
  const api = {
    getState: jest.fn(),
    setState: jest.fn(),
    subscribe: jest.fn(),
  };
  
  store = actualCreate(
    subscribeWithSelector(() => ({
      ...initialState,
    }))
  );
  
  api.getState = store.getState;
  api.setState = store.setState;
  api.subscribe = store.subscribe;
  
  return api;
};

生态系统

1. 官方中间件

# Immer 集成
npm install immer

# 持久化
npm install zustand/middleware

# 或者使用社区方案
npm install zustand-persist
npm install zustand-xstate # XState 集成

2. 常用搭配

// 1. 与 React Query 配合
const useStore = create((set) => ({
  // 本地状态
  filters: {},
  sortBy: 'date',
  
  // 服务器状态由 React Query 管理
}));

// 2. 与 Formik/Yup 配合
const useFormStore = create((set) => ({
  // ... 表单状态
}));

// 3. 与 React Router 集成
const useRouterStore = create((set) => ({
  pathname: window.location.pathname,
  navigate: (path) => {
    window.history.pushState({}, '', path);
    set({ pathname: path });
  },
}));

优缺点对比

优点:

  1. 极简 API:学习成本极低
  2. 无 Provider 地狱:不需要包裹组件
  3. 优秀的 TypeScript 支持
  4. 性能卓越:细粒度更新
  5. 中间件生态丰富
  6. 包体积极小(~1KB)

缺点:

  1. 过于灵活:缺乏强制性的最佳实践
  2. 调试相对困难(需依赖 DevTools)
  3. 缺少 Redux 的中间件生态系统

适用场景

  • 中小型 React 应用
  • 需要快速上手的项目
  • 不喜欢 Redux 复杂度的团队
  • 需要轻量级状态管理的场景
  • 与 React Query 配合使用

Zustand 是目前 React 社区最受欢迎的状态管理库之一,特别适合那些想要简单、直观且功能强大的状态管理方案的团队。