在这里插入图片描述

每日一句正能量

“内心是滤镜,选择看到光,阴影便会后退。”
当光成为主体,阴影自然退为背景。
真正的积极不是无视生活的阴影,而是深知阳光总会再次倾泻。你走的每一步都算数,时间会在未来的某个转角给你惊喜。把模糊的担忧变成可解决的小问题,用行动获得掌控感。今天是你余生中最年轻的一天,此刻就是最早的行动时刻。

前言

在 Codex 官网从单体组件向复杂交互系统演进的过程中,状态管理经历了三次痛苦的迭代。第一次,我们将所有状态塞进单个 Redux Store,结果是一个 800 行的 reducer 文件和无穷无尽的 connect 样板代码。第二次,我们全面转向 React Context,却发现主题切换的微小更新会触发整个应用树的重渲染。第三次,也就是 2026 年初的这次重构,我们建立了一套分层状态架构:局部状态用 useState,服务端状态用 TanStack Query,全局 UI 状态用 Zustand,可分享的业务状态用 URL State。这套方案将无关组件的重渲染率降低了 78%,同时将状态相关的 bug 减少了 60%。

一、状态分层:四种状态的四把钥匙

状态管理的首要原则不是"选哪个库",而是"这个状态应该放在哪里"。Codex 官网将状态划分为四个层次,每层都有明确的管理工具和边界:

局部组件状态useState / useReducer)管理表单输入、弹窗显隐、加载动画等纯组件内部数据。这类状态的作用域最小,生命周期与组件绑定,应避免过早提升。

服务端状态(TanStack Query)管理从 API 获取的文档列表、用户数据、评论内容。它本质上是服务器数据的客户端缓存,需要处理加载态、错误重试、乐观更新和后台重验证。TanStack Query 的 staleTimegcTime 配置让 Codex 的文档列表在 5 分钟内无需重复请求,同时自动处理页面重新聚焦时的数据刷新。

全局 UI 状态(Zustand)管理主题模式、侧边栏折叠、通知队列、模态框栈等跨组件共享的交互状态。这类状态变化频繁,但不需要持久化到服务端。

URL 状态searchParams)管理筛选条件、分页参数、标签页索引、排序方式等需要可分享、可回溯的业务状态。将这类状态同步到 URL,用户刷新页面不会丢失筛选结果,复制链接即可分享精确视图。

在这里插入图片描述

二、Zustand:为什么它是 2026 年的默认选择

Zustand 在德语中意为"状态",这个只有 1.2KB(gzip)的库已成为 React 生态中外部状态管理的事实标准。与 Redux 相比,它消除了样板代码;与 Context 相比,它解决了重渲染问题;与 Jotai 相比,它的 Store 模型更符合大多数团队的直觉。

Codex 官网采用按领域拆分 Store 的策略,而非将所有状态塞进一个全局对象:

// stores/uiStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface UIState {
  sidebarCollapsed: boolean;
  theme: 'light' | 'dark' | 'system';
  notificationQueue: Notification[];
  activeModal: string | null;
  toggleSidebar: () => void;
  setTheme: (theme: UIState['theme']) => void;
  pushNotification: (n: Notification) => void;
  setActiveModal: (id: string | null) => void;
}

export const useUIStore = create<UIState>()(
  persist(
    (set) => ({
      sidebarCollapsed: false,
      theme: 'system',
      notificationQueue: [],
      activeModal: null,
      toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
      setTheme: (theme) => set({ theme }),
      pushNotification: (n) => set((s) => ({
        notificationQueue: [...s.notificationQueue, n],
      })),
      setActiveModal: (id) => set({ activeModal: id }),
    }),
    {
      name: 'codex-ui-storage',
      partialize: (state) => ({ sidebarCollapsed: state.sidebarCollapsed, theme: state.theme }),
    }
  )
);

persist 中间件将 sidebarCollapsedtheme 自动同步到 localStorage,用户下次访问时偏好设置得以保留。partialize 选项确保通知队列和模态框状态不会被持久化——这些瞬态数据在页面刷新后理应重置。

Zustand 的核心优势在于无需 Provider 包裹。在根组件中直接使用 useUIStore() 即可订阅状态,框架通过代理比较机制,仅当选择器返回的值发生变化时才触发重渲染。这与 Context 的"全量广播"形成鲜明对比。

在这里插入图片描述

三、URL 作为状态来源:可分享的筛选与分页

Codex 官网的文档库页面支持按技术栈(React、Vue、Node.js 等)筛选、按发布时间排序、按标签过滤。早期这些状态存储在 Zustand 中,导致用户刷新页面后筛选条件全部丢失,也无法通过链接分享特定视图。

将筛选状态迁移到 URL 是用户体验的质变。在 Next.js App Router 中,借助 nuqs 库(原 next-usequerystate),可以实现类型安全的 URL 状态读写:

// hooks/useDocFilters.ts
'use client';

import { useQueryState, parseAsString, parseAsInteger } from 'nuqs';

export function useDocFilters() {
  const [category, setCategory] = useQueryState(
    'category',
    parseAsString.withDefault('all')
  );
  const [sortBy, setSortBy] = useQueryState(
    'sort',
    parseAsString.withDefault('newest')
  );
  const [page, setPage] = useQueryState(
    'page',
    parseAsInteger.withDefault(1)
  );
  const [tag, setTag] = useQueryState(
    'tag',
    parseAsString.withDefault('')
  );

  return {
    category, setCategory,
    sortBy, setSortBy,
    page, setPage,
    tag, setTag,
  };
}

nuqs 自动处理 URL 的序列化与反序列化,支持类型解析(字符串、整数、布尔值、数组),并在状态变化时通过 history.replaceState 更新 URL,不触发页面刷新。更关键的是,它与服务端渲染无缝协作——当用户直接访问 /docs?category=react&page=2 时,服务端可以从 searchParams 中读取筛选条件,在服务端完成数据预取,首屏 HTML 即包含过滤后的结果。

// app/docs/page.tsx
export default async function DocsPage({
  searchParams,
}: {
  searchParams: { category?: string; page?: string; tag?: string };
}) {
  const filters = {
    category: searchParams.category || 'all',
    page: Number(searchParams.page) || 1,
    tag: searchParams.tag || '',
  };

  const docs = await fetchDocs(filters); // 服务端直接预取

  return <DocList initialDocs={docs} filters={filters} />;
}

在这里插入图片描述

四、React Context 的合理使用场景

在 Zustand 成为主力后,React Context 并未被完全淘汰。Codex 官网保留了两个 Context:

ThemeContext 提供当前主题值和系统主题监听器。由于主题切换是极低频操作(用户可能一天只切换一次),Context 的重渲染成本可以忽略不计。使用 Context 而非 Zustand 的好处是:主题值可以在服务端组件中通过 use() 读取(React 19 新特性),而 Zustand Store 只能在客户端组件中访问。

AuthContext 提供用户认证壳信息(登录状态、用户 ID)。这类状态在应用生命周期中几乎不变,且需要在服务端渲染时判断路由权限。Context 的静态特性恰好匹配这一需求。

需要警惕的是将高频变化的状态放入 Context。Codex 早期曾用 Context 管理通知队列,结果每次新增通知都会触发整个应用树的重渲染。迁移到 Zustand 后,只有订阅了 notificationQueueToastContainer 组件会更新。

五、状态派生与缓存:避免不必要的计算

状态管理中的另一个性能陷阱是派生状态的重复计算。以 Codex 官网的购物车为例,总价需要根据商品列表实时计算,但不应在每次渲染时重新遍历数组。

Zustand 的选择器机制天然支持派生状态的缓存:

// stores/cartStore.ts
import { create } from 'zustand';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  addItem: (item: CartItem) => void;
  removeItem: (id: string) => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) => set((s) => ({ items: [...s.items, item] })),
  removeItem: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
}));

// 组件中使用精确选择器
export function CartSummary() {
  // 仅订阅 items 数组,而非整个 store
  const items = useCartStore((s) => s.items);

  // 使用 useMemo 缓存派生值
  const { totalPrice, totalCount } = useMemo(() => {
    return items.reduce(
      (acc, item) => ({
        totalPrice: acc.totalPrice + item.price * item.quantity,
        totalCount: acc.totalCount + item.quantity,
      }),
      { totalPrice: 0, totalCount: 0 }
    );
  }, [items]);

  return (
    <div>
      <span>{totalCount}</span>
      <span>合计 ¥{totalPrice.toFixed(2)}</span>
    </div>
  );
}

useCartStore((s) => s.items) 是关键的性能优化点。如果使用 const { items } = useCartStore() 解构整个 store,那么当 addItemremoveItem 函数引用变化时(尽管 Zustand 默认会稳定化这些函数),组件仍可能触发不必要的重渲染。精确选择器确保组件只在其真正依赖的状态切片变化时更新。

在这里插入图片描述

对于更复杂的派生逻辑,Zustand 支持通过 subscribe 创建外部派生 Store:

// 派生 Store:自动计算购物车统计
export const useCartStats = create(() => ({
  totalPrice: 0,
  totalCount: 0,
  isEmpty: true,
}));

// 在应用初始化时建立订阅
useCartStore.subscribe((state) => {
  const stats = state.items.reduce(
    (acc, item) => ({
      totalPrice: acc.totalPrice + item.price * item.quantity,
      totalCount: acc.totalCount + item.quantity,
    }),
    { totalPrice: 0, totalCount: 0 }
  );
  useCartStats.setState({
    ...stats,
    isEmpty: state.items.length === 0,
  });
});

这种模式下,CartSummary 可以直接订阅 useCartStats,完全跳过 items 数组的传递和 useMemo 的声明,派生计算在状态变化时立即执行,组件仅接收最终结果。

六、完整配置:Zustand Store 与 URL 状态同步

以下是将上述所有模式整合后的生产级配置,适用于 Codex 官网的文档筛选场景:

// components/DocFilterBar.tsx
'use client';

import { useDocFilters } from '@/hooks/useDocFilters';
import { useCallback } from 'react';

const CATEGORIES = ['all', 'react', 'vue', 'node', 'css', 'performance'];

export function DocFilterBar() {
  const { category, setCategory, sortBy, setSortBy, page, setPage } = useDocFilters();

  const handleCategoryChange = useCallback((cat: string) => {
    setCategory(cat);
    setPage(1); // 切换分类时重置到第一页
  }, [setCategory, setPage]);

  return (
    <div className="flex gap-4 items-center">
      <div className="flex gap-2">
        {CATEGORIES.map((cat) => (
          <button
            key={cat}
            onClick={() => handleCategoryChange(cat)}
            className={category === cat ? 'active' : ''}
          >
            {cat === 'all' ? '全部' : cat}
          </button>
        ))}
      </div>
      <select
        value={sortBy}
        onChange={(e) => setSortBy(e.target.value)}
      >
        <option value="newest">最新发布</option>
        <option value="popular">最受欢迎</option>
        <option value="name">名称排序</option>
      </select>
      <span>第 {page} 页</span>
    </div>
  );
}

结语

状态管理的本质不是选择最强大的工具,而是为每种状态找到最合适的容器。Codex 官网的实践验证了一个原则:局部状态用 useState,服务端状态用 TanStack Query,全局 UI 状态用 Zustand,可分享的业务状态用 URL State。这一分层架构让每种状态都待在它该在的地方,既避免了 Redux 时代的过度工程化,又规避了 Context 时代的性能陷阱。在下一篇文章中,我们将探讨前端监控体系——从性能埋点到错误追踪的完整可观测性方案。


转载自:https://blog.csdn.net/sghtgjfhv/article/details/164149552
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐