React 项目开发中,组件通信、状态管理和接口请求常常令人头大。
特别是你刚开始接触 axios、全局状态管理(useContext + useReducer)和懒加载组件时,很容易陷入「哪里发请求?哪里存数据?组件怎么更新?」的迷雾。
这篇文章,我们以一个 GitHub Repos 项目为例,一步步拆解自定义 Hook ——
useRepos的设计和用法。讲清楚它到底解决了什么问题,隐藏了哪些业务逻辑,以及你项目里该不该这么用。
🌱 场景回顾
你有一个展示 GitHub 用户仓库的组件:
<Route path="/users/:id/repos" element={<RepoList />} />
点进用户详情页后,需要根据 id 获取用户的仓库信息。
于是你在 RepoList 组件中写了:
const { id } = useParams();
const { repos, loading, error } = useRepos(id);
哎?不是用 axios.get 请求吗?为啥没看到?
🧩 useRepos 究竟做了什么?
✅ 组件里只派发一个 action:
useEffect(() => {
dispatch({ type: 'GET_REPOS', payload: id });
}, [id]);
这段逻辑意味着:“我需要获取 id 对应的仓库信息,你系统自己看着办”。
然后组件只负责用 state.repos 展示内容,并监听 loading / error 决定是否展示“加载中”或“出错啦”。
也就是说:组件不写一行 axios、不操心副作用、不管理状态。
请求逻辑去哪了?
真正的请求逻辑,被统一封装在全局状态管理中,也许像这样:
const enhancedDispatch = (action) => {
switch (action.type) {
case 'GET_REPOS':
dispatch({ type: 'LOADING_REPOS' });
getRepos(action.payload)
.then(data => dispatch({ type: 'GET_REPOS_SUCCESS', payload: data }))
.catch(err => dispatch({ type: 'GET_REPOS_ERROR', payload: err.message }));
break;
default:
dispatch(action);
}
}
这就是项目中的“中间派发逻辑”,对组件完全透明。
🧪 RepoList 是怎么消费 useRepos 的?
if (loading) return <>loading...</>
if (error) return <>Error: {error}</>
return (
<>
<h2>Repositories for {id}</h2>
{repos.map(repo => (
<Link key={repo.id} to={`/users/${id}/repos/${repo.name}`}>
{repo.name}
</Link>
))}
</>
);
逻辑是不是清晰了:
- 拿不到
id就navigate('/')重定向回首页。 - 正在加载?显示 loading。
- 报错了?显示 error。
- 成功了?用数据渲染。
你眼前的 repos 其实是全局状态 state.repos,由 useContext 提供,全局共享。
🏗️ 为什么要这样设计 useRepos?
1. 关注点分离:组件逻辑 vs 数据逻辑
useRepos只是告诉系统“我要数据”。- 系统(全局状态管理)自己决定怎么拿数据、怎么处理 loading/error。
组件专注渲染,而不是“又要写 UI,又要发请求”。
2. 逻辑复用:多个组件可复用 useRepos
比如未来还有个 ReposSidebar 也想显示当前用户仓库数目,只要:
const { repos } = useRepos(id);
无需再写 axios!这才是“业务组件”和“数据逻辑”的分离。
3. 便于测试 & 维护
你可以单独测试 getRepos、reducer 和 useRepos,而不用涉及 UI 层。
❓没有中间派发可以用 useRepos 吗?
当然可以,只不过 useRepos 就得自己写请求:
export const useRepos = (id) => {
const [repos, setRepos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!id) return;
setLoading(true);
getRepos(id)
.then(setRepos)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [id]);
return { repos, loading, error };
};
但这样每个组件的状态管理就碎片化了,维护和拓展不友好。
所以你会看到很多项目更倾向于统一 dispatch,通过全局 reducer 管理状态。
🧠 总结:useRepos 做到了什么?
- ✅ 抽离请求逻辑:组件干净
- ✅ 状态集中管理:逻辑统一
- ✅ 接口复用:组件自由组合
- ✅ 业务封装:低耦合高复用
这就是为什么越来越多项目都会用 useXxx 这样的自定义 Hook 去封装接口数据获取逻辑。
你的组件不需要关心请求、loading、报错这些细节,只要“订阅”状态。
🎉 预告:
下一篇,我们将会结合项目路由懒加载 + 动态嵌套路由,带你一步步理解 react-router 的高级玩法。
记得点赞 + 收藏,我们下一篇见!