前端状态管理的终极指南:Local State、Global State 与 Server State 的分层策略
状态管理混乱是前端项目腐化的起点。问题不在于选择了什么库,而在于是否在架构层面明确了不同状态的分层边界与归属。
一、为什么分层:三种状态,三种生命周期
前端状态按生命周期和共享范围可以明确划分为三个类别:
| Local State | 组件挂载到卸载 | 单个组件 | 表单输入值、开关状态 | useState / useReducer |
| Global State | 应用运行期(可能持久化) | 跨组件/跨路由 | 用户信息、主题、语言 | Zustand / Jotai |
| Server State | 与远程数据源同步 | 全局(缓存层) | API 数据、分页列表 | TanStack Query / SWR |
混乱的根源在于用同一套工具管理这三类状态。最常见的反模式是:把 useState 通过 props 层层传递,或者把所有数据全部塞进全局 Store。
二、Local State:让状态待在它该待的地方
Local State 的管理原则:状态应该在需要它的最小作用域内声明。
/**
* Local State 最佳实践:按职责拆分组件,避免"上帝组件"
* 错误示例对比正确示例
*/
// 不推荐:将所有状态集中在一个大组件中
function SearchPageBad(): JSX.Element {
const [keyword, setKeyword] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedFilter, setSelectedFilter] = useState('all');
const [page, setPage] = useState(1);
// 大量混合逻辑…
return <div>{/* 所有 UI 堆在一起 */}</div>;
}
// 推荐:按功能域拆分为独立的小组件,各自管理自己的 Local State
function SearchPage(): JSX.Element {
return (
<div>
<SearchInput />
<SearchFilters />
<SearchResults />
</div>
);
}
function SearchInput(): JSX.Element {
const [keyword, setKeyword] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
// 搜索建议的相关逻辑只在这里
const handleInputChange = (value: string): void => {
setKeyword(value);
if (value.length < 2) {
setSuggestions([]);
return;
}
// 调用搜索建议 API…
};
return (
<div className="search-input-wrapper">
<input
value={keyword}
onChange={(e) => handleInputChange(e.target.value)}
aria-label="搜索关键词"
/>
{suggestions.length > 0 && (
<ul role="listbox">
{suggestions.map((item, idx) => (
<li key={idx} role="option">{item}</li>
))}
</ul>
)}
</div>
);
}
useReducer 使用时机: 当组件内状态更新逻辑复杂(多个子值相互依赖,或状态转换有业务语义)时,用 useReducer 替代多个 useState。
/**
* useReducer 适用场景:表单多步骤向导
* 多个状态字段在步骤切换时存在联动关系
*/
type WizardStep = 'personal' | 'address' | 'confirm';
interface FormState {
step: WizardStep;
name: string;
email: string;
city: string;
isSubmitting: boolean;
error: string | null;
}
type FormAction =
| { type: 'SET_FIELD'; field: keyof FormState; value: string }
| { type: 'NEXT_STEP' }
| { type: 'PREV_STEP' }
| { type: 'SUBMIT_START' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_ERROR'; error: string };
const STEP_ORDER: WizardStep[] = ['personal', 'address', 'confirm'];
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_FIELD':
return { …state, [action.field]: action.value, error: null };
case 'NEXT_STEP': {
const currentIdx = STEP_ORDER.indexOf(state.step);
if (currentIdx >= STEP_ORDER.length – 1) return state;
return { …state, step: STEP_ORDER[currentIdx + 1] };
}
case 'PREV_STEP': {
const currentIdx = STEP_ORDER.indexOf(state.step);
if (currentIdx <= 0) return state;
return { …state, step: STEP_ORDER[currentIdx – 1] };
}
case 'SUBMIT_START':
return { …state, isSubmitting: true, error: null };
case 'SUBMIT_SUCCESS':
return { …state, isSubmitting: false };
case 'SUBMIT_ERROR':
return { …state, isSubmitting: false, error: action.error };
default:
return state;
}
}
三、Global State:让共享最小化、可追溯
Global State 的管理原则:能不放全局就不放全局,放进去的要可追溯。
/**
* 使用 Zustand 管理全局状态的推荐模式
* 核心原则:
* 1. 按领域拆分 Store,不要一个 Store 装所有数据
* 2. Action 命名体现业务语义,而非 setXxx
* 3. 提供 selector 减少不必要的重渲染
*/
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// 拆分 Store 示例:用户信息独立管理
interface UserStore {
user: { id: string; name: string; role: string } | null;
isAuthenticated: boolean;
login: (credentials: { username: string; password: string }) => Promise<void>;
logout: () => void;
}
export const useUserStore = create<UserStore>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: async (credentials) => {
// 实际项目中应调用 API
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error(`登录失败: ${response.status}`);
}
const user = await response.json();
set({ user, isAuthenticated: true });
},
logout: () => {
set({ user: null, isAuthenticated: false });
},
}),
{
name: 'user-storage', // localStorage key
partialize: (state) => ({ user: state.user }), // 只持久化 user,不持久化 isAuthenticated
}
)
);
// 主题 Store 独立管理
interface ThemeStore {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
export const useThemeStore = create<ThemeStore>()(
persist(
(set) => ({
theme: 'light',
toggleTheme: () =>
set((state) => ({
theme: state.theme === 'light' ? 'dark' : 'light',
})),
}),
{ name: 'theme-storage' }
)
);
// 组件中使用 selectors 减少渲染
function UserAvatar(): JSX.Element {
// 只订阅 name,user.role 变化不会触发重渲染
const userName = useUserStore((state) => state.user?.name ?? '未登录');
return <span>{userName}</span>;
}
四、Server State:把缓存、同步和过期交给框架
Server State 的管理原则:数据的主人不是前端,是服务器。所有"从服务器获得、要展示给用户、未来可能变化"的数据,都是 Server State。
/**
* 使用 TanStack Query 管理 Server State 的生产级封装
* 核心价值:自动缓存、后台刷新、乐观更新、分页/无限滚动
*/
import {
useQuery,
useMutation,
useQueryClient,
UseQueryOptions,
} from '@tanstack/react-query';
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
interface ProductListParams {
page: number;
pageSize: number;
keyword?: string;
category?: string;
}
/**
* 通用 API 请求函数
* 统一处理错误码,抛出可读的错误信息
*/
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
…options,
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(
`API 请求失败 [${response.status}]: ${errorBody.message || '未知错误'}`
);
}
return response.json();
}
/**
* 获取产品列表(使用 React Query 缓存和分页)
*/
export function useProductList(params: ProductListParams) {
return useQuery<Product[]>({
queryKey: ['products', params],
queryFn: async () => {
const searchParams = new URLSearchParams();
searchParams.set('page', String(params.page));
searchParams.set('pageSize', String(params.pageSize));
if (params.keyword) searchParams.set('keyword', params.keyword);
if (params.category) searchParams.set('category', params.category);
return apiFetch<Product[]>(`/api/products?${searchParams.toString()}`);
},
// 数据保持 5 分钟新鲜,期间不重新请求
staleTime: 5 * 60 * 1000,
// 页面不可见时,后台仍保持数据刷新
refetchOnWindowFocus: true,
// 错误重试:最多 3 次,指数退避
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
}
/**
* 更新产品库存(乐观更新示例)
*/
export function useUpdateStock() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, stock }: { id: string; stock: number }) => {
return apiFetch<Product>(`/api/products/${id}`, {
method: 'PATCH',
body: JSON.stringify({ stock }),
});
},
// 乐观更新:不等服务器响应就更新 UI
onMutate: async ({ id, stock }) => {
// 取消正在进行的查询,防止覆盖乐观更新
await queryClient.cancelQueries({ queryKey: ['products'] });
// 保存旧数据用于回滚
const previousData = queryClient.getQueryData<Product[]>(['products']);
// 乐观更新缓存中的数据
queryClient.setQueryData<Product[]>(['products'], (old) => {
if (!old) return old;
return old.map((p) => (p.id === id ? { …p, stock } : p));
});
return { previousData };
},
// 如果服务器返回错误,回滚到旧数据
onError: (_err, _vars, context) => {
if (context?.previousData) {
queryClient.setQueryData(['products'], context.previousData);
}
},
// 无论成功失败,重新拉取最新数据
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
五、总结
状态管理的分层策略本质上是一种"职责划分":
- Local State 让组件自给自足,不要向上渗透;
- Global State 控制在最小共享范围,用领域拆分代替大杂烩 Store;
- Server State 让专业工具处理缓存、同步和一致性,不要手写 useEffect + fetch。
当你开始新功能时,先问自己:这份数据的"真实归属"在哪里?答案会直接告诉你应该把它放在哪一层。
本文的 Zustand 示例参考了 pmndrs/zustand 文档,TanStack Query 示例参考了 TanStack 官方文档。
资料说明
本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论,不应视为行业事实。可参考 0730 资料来源索引,并在发布前将具体来源贴到对应断言之后。


