前言:外表光鲜,内里一团糟?
接着上一篇聊。咱们用 Vite 秒开了项目,用 Tailwind 把界面画得像模像样。现在的你,看着浏览器里那个漂亮的 Dashboard,是不是心里美滋滋的?
但是,只有你自己知道,打开代码编辑器后,那是怎样的一番景象。
UserContext 文件里写了 500 行,里面混杂了用户信息、主题色、侧边栏开关,还有两三个 API 请求的逻辑。
组件里满屏的 useEffect,为了同步一个数据,你不得不把 props 传了五层。
有时候用户切了一下 Tab 再回来,数据没刷新;有时候明明只是点了个弹窗,后台却莫名其妙发了三个请求。
这就是典型的**“数据流打结”**。
今天,我们要来做一次彻底的“外科手术”。我们要理清 React 状态管理的终极奥义:Server State(服务端状态) 与 Client State(客户端状态) 的边界。
只要搞懂了这个,你代码里 80% 的混乱都会消失。
误区:所有数据都是“状态”?
很多兄弟(包括当年的我)写 React 有个惯性思维: “只要是数据,就要找个地方存起来,最好是全局的,比如 Redux 或者 Context。”
于是,你把后端返回的 userList,和你前端自己控制的 isModalOpen,全部扔进了一个大染缸里。
这就好比你把借来的书(Server State)和自己买的内裤(Client State)都塞进了同一个抽屉。 借来的书是有时效性的,别人改了你可能不知道,还得定期还; 而内裤是你私有的,完全由你控制。
混在一起的结果就是:你想找内裤的时候,翻出来一堆过期的书。
概念拆解:上帝的归上帝,凯撒的归凯撒
我们要把“状态”劈成两半。
1. Server State(服务端状态)
- 来源:后端 API。
- 特点:它不属于你。你只是把数据“借”过来展示一下。服务器上的数据随时可能被别人修改。
- 需求:你需要处理缓存、去重、定时更新、错误重试。
- 例子:用户列表、商品详情、订单记录。
2. Client State(客户端状态)
- 来源:用户的交互。
- 特点:它完全属于你。它是同步的,只存在于当前浏览器会话中。
- 需求:你需要轻量级的共享。
- 例子:侧边栏展开/收起、弹窗的 Open/Close、深色模式切换、表单输入的临时内容。
解决方案:React Query 掌管外部,Context 掌管内部
❌ 错误示范:手动挡管理 Server State
这是我们最常干的事,在 Context 里手写 fetch:
// UserContext.tsx
const UserProvider = ({ children }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 典型的“手动挡”:自己处理时机,自己处理竞态,还容易写出 Bug
const fetchUser = async () => {
setLoading(true);
try {
const res = await api.getUser();
setData(res);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchUser(); }, []);
return (
<UserContext.Provider value={{ data, loading, error, refetch: fetchUser }}>
{children}
</UserContext.Provider>
);
};
这就叫既当爹又当妈。Context 本来只是用来传输数据的管道,你却非要让它负责生产数据。
✅ 正确示范:React Query (TanStack Query) 接管
Server State 的事,交给专业的来。
import { useQuery } from '@tanstack/react-query';
const UserProfile = () => {
// 一行代码,搞定缓存、去重、Loading、Error、自动刷新
const { data, isLoading, error } = useQuery({
queryKey: ['user'],
queryFn: api.getUser,
});
if (isLoading) return <div>加载中...</div>;
// ...
};
你原本那几百行 Context 代码,现在直接删掉。 你想在别的地方也用这个数据?复制这行 useQuery 就行。React Query 会自动去缓存里找,不会重复发请求。
那么,Context 剩下干嘛?
把 Server State 剥离出去后,Context 终于可以干它该干的事了:管理纯粹的 UI 状态。
比如,一个全局的主题控制器:
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
// 这才是 Client State:同步、简单、完全由前端控制
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
这就清爽多了。Context 变得非常轻量,不容易造成不必要的重渲染。
实战判断法则
下次你定义 state 的时候,问自己三个问题:
-
这数据是从 API 拿的吗?
- 是 -> React Query (Server State)。
- 否 -> 下一题。
-
这数据需要在整个 App 里共享吗?
- 是(比如主题、用户信息) -> Context (Client State)。
- 否(比如下拉框是否展开) -> useState / useReducer (Local State)。
-
这数据需要持久化吗?
- 是 -> 配合
localStorage或者 URL 参数(URL State,这个以后聊)。
- 是 -> 配合
总结
别再让你的 Context 变成垃圾场了。
当我们把 Server State 移交给 React Query 之后,你会发现:
- Props Drilling 变少了:因为任何组件都可以直接
useQuery拿数据,不需要父组件传下来。 - useEffect 变少了:因为不需要你在组件挂载时手动触发 fetch。
- 代码删了一半:那些手动维护 loading/error 的逻辑全都没了。
让上帝的归上帝,让 React Query 的归 React Query。 你的代码会因此变得清晰而优雅。
好了,我要去把项目里那个还在用 Redux 存 API 数据的代码山重构了,祝大家的数据流永远顺畅。
下期预告:状态理清了,但有时候组件本身还是很难用。比如一个复杂的
Tab组件,你是不是传了一堆activeTab,onTabChange,tabStyle这种 props? 下一篇,我们来聊聊 “React 复合组件模式 (Compound Components)” 。看看像<Select>和<Option>这种原生标签,是如何教我们设计出既灵活又强大的组件 API 的。