2025-2026 react 后台黄金架构

171 阅读7分钟

如果你做的是后台管理系统,那么这套:
Zustand(client state)+ React Query(server state)+ Ant Design + Vite + TailwindCSS
就是目前前端圈里最优雅、最快速、最轻量的组合拳。
我是于晏,我为这套后台系统带盐。

比传统的 Redux 架构——
✔ 更快
✔ 更轻
✔ 更不啰嗦
✔ 更适合实际业务

这是我心目中,真正的前端后台最佳实践

image.png


01 前言:后台管理系统的最佳实践到底是什么?

兄弟们,现在 2025 年了,做后台管理系统再用 Redux Toolkit + thunk 这种老派写法,真的就是——

在用坦克打蚊子。
有用,但太重了,太累了,不优雅。

后台系统的特点是:

  • 大量列表页
  • 大量筛选条件
  • 大量 server state(接口请求数据)
  • 少量 client state(本地 UI 状态)
  • 高开发效率需求

所以需要这样一套组合:

✔ 状态管理:Zustand(client state)

轻、快、简单,写法优雅,不用写 reducer、不用写 action、不用 dispatch。

数据请求:React Query(server state)

自动缓存、自动请求、自动状态管理、自动错误处理、自动刷新,爽到飞起。

UI:Antd

无需多盐,后台最稳 UI 库。

工程化:Vite + TailwindCSS

启动快,更新快,样式写起来像打字一样流畅。

一句话总结:

客户端状态 → zustand
服务端数据 → zustand
页面 UI → antd
构建工具 → vite

这就是行业最佳实践🤣。

上边我说的不一定都真,信bro你会很惨的
真实原因:最近没活,给自己找事干,什么性能呀,lighthouse呀这些都粗略看过了


02 项目搭建:Vite + Antd + TailwindCSS(保姆级别)

彦祖已经写过一篇超详细搭建教程:
🫱 juejin.cn/post/752232…

略。


03 为什么是 Zustand?

zustand地址 : zustand.docs.pmnd.rs/getting-sta…

兄弟们,首先我们来夸夸 Zustand
这个玩意真的太可爱了。

它看起来:

  • 小小只 🐹
  • API 简单到离谱
  • 写起来超自由

但实际上:

  • 不会出现 react-redux 的僵尸 child 问题
  • 支持 React 并发模式
  • 性能极高
  • 没有 Provider 嵌套地狱,不用像react-redux一样全局包裹仓库
  • 语法直接爽到爆,少了好多代码,slice,thrunk,reducer通通不需要

一句话:
Zustand 是 React Redux 的平替,而且还更好用。你爱了吗,我的兄弟

image.png


04 Zustand — 使用姿势

Step 1:安装

npm install zustand

Step 2:一个最简单的示例

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increase: () => set((s) => ({ bears: s.bears + 1 })),
  removeAll: () => set({ bears: 0 })
}))

Step 3:组件里直接使用

function BearCounter() {
  const bears = useBearStore((s) => s.bears)
  const increase = useBearStore((s) => s.increase)

  return (
    <>
      <h1>{bears}</h1>
      <button onClick={increase}>+1</button>
    </>
  )
}

是不是爽?没有 reducer、action、dispatch。


🍯 可选:持久化 (Persist Middleware)

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

export const useFishStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: "food-storage",
      storage: createJSONStorage(() => sessionStorage),
    }
  )
);

不写 storage 就是默认 localStorage

❓有靓仔可能会问Zustand vs localStorage/sessionStorage?

我的简单理解:

  • Zustand = JS 内存(页面刷新清空)
  • localStorage = 浏览器硬盘存(永久)
  • sessionStorage = 浏览器会话存(关闭标签页清空)

通过中间件可自动持久化 → localStorage/sessionStorage。 zustand(react-redux)的仓库是js运行中的存储,刷新关闭页面会消失,重新加载就会重新挂载,可以通过中间件把仓库变为 localStorage( sessionStorag)存储


05 为什么是 React Query(TanStack Query)?

TanStack Query地址:tanstack.com/query/lates…

React Query 是:

React 缺失的 server-state 管家。
前两年叫react query,不知道为啥改名了,我理解为它支持了vue和其它一些框架,所以改名为了TanStack Query

它会帮你自动处理:

  • 请求缓存
  • 请求去重
  • 自动重试
  • 自动刷新
  • 预取
  • 错误状态
  • loading 状态
  • 页面回到焦点时重新请求
  • 失效缓存自动刷新

这不比你自己写 axios + useEffect 爽 1000 倍?

不写useEffect简直不要太爽!!!

简直是后台项目的神(YYDS)。

image.png


06 React Query 基础用法(保姆级)

安装

npm i @tanstack/react-query @tanstack/react-query-devtools
npm i -D @tanstack/eslint-plugin-query

Provider 挂载

const queryClient = new QueryClient()

root.render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools />
  </QueryClientProvider>
)

一个最简单例子

const { data, isLoading } = useQuery({
  queryKey: ["repo"],
  queryFn: () =>
    fetch("https://api.github.com/repos/tannerlinsley/react-query")
      .then(r => r.json())
})

07 React Query + Zustand:天衣无缝的组合

真正的后台系统是:

第一步:Zustand 保存筛选条件
第二步:React Query 根据筛选条件请求数据
第三步:Table 渲染

就像下面这样


08 实战案例:金币流水查询(完整流程讲解)

我简单写一个我页面吧


第一步:react query 挂载

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { App as AntdApp, ConfigProvider } from "antd";
import { StyleProvider } from "@ant-design/cssinjs";
import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ENV } from "./utils/env.ts";

// 1️⃣ 创建 React Query Client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5分钟缓存,避免重复请求
      // @ts-ignore
      cacheTime: 1000 * 60 * 60 * 24, // 24小时缓存
      refetchOnWindowFocus: false, // 可根据需要开启或关闭
    },
  },
});

// 2️⃣ 持久化配置到 localStorage
// const persister = createAsyncStoragePersister({
//   storage: window.localStorage,
// });

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <QueryClientProvider
      client={queryClient}
      // persistOptions={{ persister }}
    >
      <ConfigProvider locale={zhCN}>
        <StyleProvider hashPriority="high">
          <AntdApp>
            <App />
          </AntdApp>
        </StyleProvider>
      </ConfigProvider>
      {ENV === "DEV" && <ReactQueryDevtools initialIsOpen={false} />}
    </QueryClientProvider>
  </StrictMode>
);

ENV === "DEV" && ‘ReactQueryDevtools ’ 在开发环境打开可视化工具

第二步:API 封装

export type GetGoldFlowParams = {
  begin_date: string;
  end_date: string;
  search_user_id: number | string | null;
  gold_source: string | number;
  page: number;
  page_size: number;
  consumption_or_income: string | number | null;
  currency_type: string | number | null;
};

export function getGoldFlow(params: GetGoldFlowParams) {
  return request("management/user_gold_log", params, "post");
}

第三步:Zustand 保存筛选状态

export const useGoldFlowStore = create((set, get) => ({
  params: defaultParams,
  submitParams: defaultParams,
  shouldQuery: false,

  updateParams: (d) => set((s) => ({ params: { ...s.params, ...d } })),
  submit: () =>
    set((s) => ({
      submitParams: s.params,
      shouldQuery: true,
    })),
}));

Zustand 管:

  • 当前筛选条件 params
  • 最终提交参数 submitParams
  • 是否触发请求 shouldQuery

第四步:React Query 获取数据

export function useGoldFlowQuery(params) {
  const shouldQuery = useGoldFlowStore((s) => s.shouldQuery);

  return useQuery({
    queryKey: ["goldFlow", params],
    enabled: shouldQuery,
    queryFn: async () => {
      const res = await getGoldFlow(params);
      return res.data;
    },
  });
}

只要 submit() 触发,React Query 就开始请求。


第五步:Filter 页(筛选表单)

// Filter.tsx
import { Card, Space, Button, InputNumber, Select, DatePicker } from "antd";
import dayjs from "dayjs";
import { useConfig } from "@/features/config/useConfig";
import { useGoldFlowStore } from "@/features/goldFlow/store";

const { RangePicker } = DatePicker;

export default function Filter() {
  const { params, updateParams, submit } = useGoldFlowStore();
  const { data: config } = useConfig();

  return (
    <Card>
      <Space wrap size="large" className="w-full">
        <Space wrap size="large">
          <label>
            <span>时间:</span>
            <RangePicker
              value={[dayjs(params.begin_date), dayjs(params.end_date)]}
              allowClear={false}
              onChange={(_, dateStr) =>
                updateParams({
                  begin_date: dateStr[0],
                  end_date: dateStr[1],
                  page: 1,
                })
              }
            />
          </label>

          <label>
            <span>用户ID:</span>
            <InputNumber
              controls={false}
              value={params.search_user_id ?? undefined}
              onChange={(v) => updateParams({ search_user_id: v, page: 1 })}
              className="max-w-[120px]"
            />
          </label>

          <label>
            <span>货币类型:</span>
            <Select
              allowClear
              className="min-w-[120px]"
              value={params.currency_type}
              options={[
                { label: "全部", value: "" },
                ...(config?.currency_type ?? []),
              ]}
              onChange={(v) => updateParams({ currency_type: v, page: 1 })}
            />
          </label>

          <label>
            <span>收入/支出:</span>
            <Select
              allowClear
              className="min-w-[120px]"
              value={params.consumption_or_income}
              options={[
                { label: "全部", value: "" },
                ...(config?.consumption_or_income ?? []),
              ]}
              onChange={(v) =>
                updateParams({ consumption_or_income: v, page: 1 })
              }
            />
          </label>

          <label>
            <span>消耗支出详情:</span>
            <Select
              allowClear
              className="min-w-[120px]"
              value={params.gold_source}
              options={[
                { label: "全部", value: "" },
                ...(config?.gold_source ?? []),
              ]}
              onChange={(v) => updateParams({ gold_source: v, page: 1 })}
            />
          </label>
        </Space>

        <Space>
          <Button type="primary" onClick={submit}>
            查询
          </Button>
        </Space>
      </Space>
    </Card>
  );
}



第六步:MTable 页(表格 + 分页) Pannel页

React Query 自动帮你缓存数据,分页切换流畅无比。

table页面:

// MTable.tsx
import { Table } from "antd";
import { v4 } from "uuid";
import { useGoldFlowStore } from "@/features/goldFlow/store";
import { useGoldFlowQuery } from "@/features/goldFlow/query";
import { GoldFlowItem } from "@/features/goldFlow/types";

export default function MTable() {
  const { params, updateParams } = useGoldFlowStore();
  const { data, isLoading } = useGoldFlowQuery(params);

  const res_data =
    data?.res_data?.map((item: GoldFlowItem) => ({ ...item, uuid: v4() })) ??
    [];

  const columns = [
    { title: "时间", dataIndex: "date_time" },
    { title: "用户ID", dataIndex: "user_id" },
    { title: "地区", dataIndex: "country_code" },
    {
      title: "货币类型",
      dataIndex: "currency_type",
      render: (val: number) => (val === 1 ? "金币" : "钻石"),
    },
    { title: "消耗支出详情", dataIndex: "source_type" },
    { title: "数量", dataIndex: "amount" },
    { title: "余额", dataIndex: "amount_after" },
    { title: "广告价值均值", dataIndex: "ad_revenue_avg" },
    { title: "备注", dataIndex: "comment" },
  ];

  return (
    <Table
      columns={columns}
      dataSource={res_data}
      loading={isLoading}
      rowKey="uuid"
      size="small"
      bordered
      scroll={{ x: "max-content" }}
      pagination={{
        total: data?.count ?? 0,
        current: params.page,
        pageSize: params.page_size,
        onChange: (page, page_size) => updateParams({ page, page_size }),
      }}
    />
  );
}

pannel页面

// Pannel.tsx
import { Space } from "antd";
import Filter from "../Filter";
import MTable from "../Table";

export default function Pannel() {
  return (
    <Space direction="vertical" size="large" className="w-full">
      <Filter />
      <MTable />
    </Space>
  );
}


image.png

image.png


09 最佳实践总结

最后我们总结后台最佳实践:

Client State → Zustand

  • UI 状态
  • 表单筛选
  • modal 开关
  • 当前页码
  • 不与 server 同步的数据

Server State → React Query

  • 列表接口
  • 详情接口
  • 缓存数据
  • 分页与请求去重
  • 自动 loading/error

Antd + Tailwind → UI 快速开发

一个写组件一个写样式,效率拉满。

Vite → 工程极速

开发体验比 webpack 舒坦多了。


10 写在最后:学会这一套,你就超过 80% 前端

兄弟,你只要把:

  • Zustand
  • React Query
  • Antd
  • Tailwind
  • Vite

这套完全吃透,基本后台系统的所有业务你都能优雅搞定。

而且写法干净、扩展性强、性能高,这套就是 2025-2026 年后台项目的黄金架构

不是最佳黄金架构来找我对线😁

有什么问题后续我持续更新,因为文档内容还是挺多的,很多东西没看完🤦‍♂️🤦‍♂️🤦‍♂️