如果你做的是后台管理系统,那么这套:
Zustand(client state)+ React Query(server state)+ Ant Design + Vite + TailwindCSS
就是目前前端圈里最优雅、最快速、最轻量的组合拳。
我是于晏,我为这套后台系统带盐。
比传统的 Redux 架构——
✔ 更快
✔ 更轻
✔ 更不啰嗦
✔ 更适合实际业务
这是我心目中,真正的前端后台最佳实践。
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 的平替,而且还更好用。你爱了吗,我的兄弟
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)。
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>
);
}
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 年后台项目的黄金架构。
不是最佳黄金架构来找我对线😁
有什么问题后续我持续更新,因为文档内容还是挺多的,很多东西没看完🤦♂️🤦♂️🤦♂️