@[toc]
如果你写 RN 写到后面,开始出现下面这些情况:
- 一个页面引用 5~8 个自定义 Hook
- 一个 Hook 内部 300 行,还不敢拆
- useEffect 嵌套 useEffect,依赖数组随缘写
- 改一个状态,引发一连串莫名其妙的更新
那问题往往不在 React Native,而在 Hooks 的设计方式本身已经失控了。
这篇文章不讲"Hook 是什么",而是讲:
在真实 RN 项目里,Hooks 应该怎么设计,哪些写法一定会把项目带沟里。
一、为什么 RN 项目里的 Hook 特别容易"写炸"?
先说一个现实结论:
Hook 是"状态和行为的放大器",不是解药。
1. Hook 太容易承载"过多职责"
很多项目里的 Hook,最后都会长成这样:
ts
function usePageLogic() {
// 请求
// 权限判断
// 数据转换
// 埋点
// 页面跳转
}
写的时候很爽,
三个月后------没人敢改。
2. useEffect 本身就是"隐性依赖地狱"
ts
useEffect(() => {
doSomething(a, b, c)
}, [a, b])
- 少写依赖 → 逻辑不一致
- 多写依赖 → 无限触发
- eslint 一关 → 世界和平(假的)
3. RN 的异步 + 生命周期放大问题
- 页面切后台
- 组件卸载
- 网络请求返回
Hook 如果没设计好,很容易:
- setState on unmounted component
- 重复请求
- 内存泄漏
二、一个健康 Hook 的设计目标
在进入规范前,先给你一个判断标准。
一个"设计良好"的 Hook,应该满足:
- 职责单一
- 可预测
- 可组合
- 不隐藏业务规则
如果一个 Hook 做不到这四点,迟早要拆。
三、Hook 设计的 5 条核心规范
规范 1:Hook 只做"状态编排",不做业务裁决
错误示例:
ts
function useUserPermission(user) {
if (user.role === 'admin') {
// ...
}
}
正确做法:
ts
// domain/user.ts
export function canEdit(user: User) {
return user.role === 'admin'
}
// hooks/useUserPermission.ts
export function useUserPermission(user: User) {
return canEdit(user)
}
判断规则属于 domain,不属于 hook。
规范 2:一个 Hook 只管理一类状态
反例(很常见):
ts
function usePage() {
const [list, setList] = useState([])
const [loading, setLoading] = useState(false)
const [selected, setSelected] = useState(null)
}
正确拆法:
ts
useListData()
useLoadingState()
useSelection()
Hook 是组合单位,不是容器。
规范 3:不要在 Hook 里偷偷"改全局"
危险写法:
ts
function useInit() {
useEffect(() => {
store.setState(...)
}, [])
}
问题是:
- 页面一 mount 就改全局
- 调用方完全无感知
更好的方式是:
ts
function useInit() {
return () => {
store.setState(...)
}
}
副作用必须显式触发。
规范 4:异步 Hook 必须考虑"取消和卸载"
错误示例:
ts
useEffect(() => {
fetchData().then(setData)
}, [])
改进版本:
ts
useEffect(() => {
let mounted = true
fetchData().then(res => {
if (mounted) setData(res)
})
return () => {
mounted = false
}
}, [])
这是 RN 项目里非常真实的坑。
规范 5:Hook 返回值要"语义清晰"
不要这样:
ts
const [a, b, c] = useSomething()
推荐这样:
ts
const {
data,
loading,
refresh
} = useSomething()
Hook 是接口,不是数组谜题。
四、useEffect 的正确使用姿势
一句话总结:
能不用 useEffect,就不用。
1. 能同步算出来的,不要进 effect
错误:
ts
useEffect(() => {
setTotal(price * count)
}, [price, count])
正确:
ts
const total = price * count
2. 派生状态 > effect 驱动状态
ts
const filteredList = useMemo(() => {
return list.filter(...)
}, [list])
3. effect 只做三件事
- 网络请求
- 订阅 / 监听
- 与外部系统交互
其他情况,大概率是设计问题。
五、常见 Hook 反模式清单(重点)
反模式 1:巨型 Hook
ts
usePageLogic() // 400 行
症状:
- 无法复用
- 无法测试
- 无法拆
反模式 2:useEffect 充当"状态中转站"
ts
useEffect(() => {
setB(calcA(a))
}, [a])
这基本等同于:
- 手写响应式系统
- 极易失控
反模式 3:Hook 内部偷偷导航
ts
navigation.navigate(...)
页面不知道:
- 什么时候跳
- 为什么跳
反模式 4:Hook 强依赖页面结构
ts
useScrollPosition(ref)
ref 来自页面,生命周期复杂,极易出问题。
六、一个可运行的 Demo:拆解"失控 Hook"
原始写法
ts
function useProfilePage() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetchUser().then(res => {
setUser(res)
setLoading(false)
})
}, [])
return { user, loading }
}
拆解后
ts
function useUserData() {
const [user, setUser] = useState(null)
useEffect(() => {
fetchUser().then(setUser)
}, [])
return user
}
function useLoading(initial = false) {
const [loading, setLoading] = useState(initial)
return { loading, setLoading }
}
页面组合:
ts
const user = useUserData()
const { loading } = useLoading()
逻辑更清晰,也更容易复用。
七、真实项目里的变化
在一个中大型 RN 项目中,重构 Hooks 后:
- useEffect 数量 ↓ 40%+
- 页面逻辑可读性明显提升
- 新人调试 Hook 成本大幅降低
- "一改就炸"的情况明显减少
最后的总结
如果你记住这三点就够了:
- Hook 不是垃圾桶
- 业务规则永远不属于 Hook
- useEffect 是最后手段,不是默认选择
Hook 写得好,RN 项目会非常优雅;
Hook 写得乱,TS、Redux、架构都救不了你。