@[toc]
如果你做过稍微复杂一点的 RN 列表,一定遇到过这种情况:
- 点赞卡
- 展开卡
- 勾选卡
- 编辑卡
一开始觉得都是"小状态",就随手往上提:
- 提到列表组件
- 再提到页面
- 最后进 Redux / Context
然后某一天你发现:
明明只点了一个 item,整个列表却跟着一起抖了一下。
这篇我们就专门拆:
RN 列表里,状态到底该放哪?
一、先说一个容易被忽略的前提
在 RN 里:
render 本身就是成本。
不像 Web:
- DOM diff 快
- 浏览器能兜底
- 掉帧不一定立刻可感知
RN 是:
- JS 线程 + UI 线程
- render 和交互强耦合
- 一点浪费就能直接掉帧
所以"状态放哪"这件事,本质是渲染影响面的问题。
二、一个非常典型的错误结构
场景
一个点赞列表,每个 item 都可以点 👍。
问题代码
tsx
function Page() {
const [likedIds, setLikedIds] = useState<string[]>([]);
const toggleLike = (id: string) => {
setLikedIds((prev) =>
prev.includes(id)
? prev.filter((i) => i !== id)
: [...prev, id]
);
};
return (
<FlatList
data={data}
renderItem={({ item }) => (
<Item
item={item}
liked={likedIds.includes(item.id)}
onLike={toggleLike}
/>
)}
/>
);
}
问题不在逻辑,而在位置。
三、一次点赞,到底触发了什么?
我们走一遍真实链路。
1. 点赞触发 Page 的 setState
ts
setLikedIds(...)
2. Page 组件 re-render
- FlatList 重新执行
- renderItem 重新创建
3. 所有 Item 都被重新计算 props
即使:
- 只有一个 id 变化
- 99 个 item 的 liked 值没变
4. JS 线程压力瞬间放大
你会看到:
- 滑动卡顿
- FPS 掉到 40 以下
- 但 FlatList 本身"什么都没做错"
四、这里的根本问题是什么?
一句话:
你把「局部状态」升级成了「全局状态」。
点赞状态本质是:
- 只影响当前 item
- 不影响列表结构
- 不影响其他 item
但你却让它:
- 由 Page 管理
- 参与列表整体 render
五、什么是 RN 列表里的"局部状态"?
判断标准非常简单。
如果满足下面 3 条,几乎一定是局部状态:
- 只影响单个 item 的 UI
- 不影响列表排序 / 数量
- 不需要被其他页面感知
比如:
- 点赞是否高亮
- 展开/收起
- 输入框内容
- 临时动画状态
六、正确写法:状态下沉到 Item
改造后的代码
tsx
const Item = React.memo(function Item({ item }) {
const [liked, setLiked] = useState(false);
return (
<View style={{ padding: 16 }}>
<Text>{item.title}</Text>
<TouchableOpacity onPress={() => setLiked((v) => !v)}>
<Text>{liked ? '👍 已点赞' : '👍 点赞'}</Text>
</TouchableOpacity>
</View>
);
});
发生了什么变化?
- 点赞只触发当前 Item render
- FlatList 完全不动
- JS 线程压力极小
七、那全局状态到底该放什么?
很多人会走到另一个极端:
那是不是状态都应该放 item 里?
当然不是。
在 RN 列表里,全局状态通常只有三类:
1. 影响列表结构的
- 排序
- 过滤条件
- 分组规则
2. 影响列表数据源的
- 请求结果
- 分页游标
- 服务端状态
3. 跨页面共享的
- 用户身份
- 权限
- 全局配置
八、一个边界模糊但很常见的例子
勾选列表(批量操作)
很多人第一反应:
勾选状态是不是局部?
答案是:一半一半。
- 单个 item 的勾选是局部
- 勾选集合(选了几个)是全局
合理拆法
tsx
function Page() {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const toggle = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
return (
<FlatList
data={data}
renderItem={({ item }) => (
<Item
item={item}
selected={selectedIds.has(item.id)}
onToggle={toggle}
/>
)}
/>
);
}
这里的关键是:
- 集合是全局
- UI 变化是局部
- 不要在 Page 里做 UI 逻辑
九、为什么 RN 对"状态位置"这么敏感?
核心原因只有一个:
RN 没有浏览器级别的性能兜底。
- 每次 render 都是真金白银
- JS 线程一忙,交互立刻受影响
- 列表又是 render 密集区
所以 RN 项目天然会逼你思考:
- 状态是不是放太高了?
- 这个状态真的需要全局吗?
十、从 RN 反看 Web,其实也是好习惯
如果你用 RN 的标准去看 Web 项目,会发现:
- 很多 Vue / React Web 项目状态提得太高
- Context / provide 被滥用
- 列表渲染边界模糊
只是 Web 帮你兜住了而已。
十一、一句话总结
如果只记住一句:
RN 列表性能的关键,不是用什么组件,而是状态影响了多少 render。
局部状态就待在 item 里,
全局状态只管结构和数据。