常见反模式与坑
本教程共 50 篇 · 第 49 篇 · 更新于 2026-07-29 · 约 9 分钟阅读
49. 常见反模式与坑
本节目标:认识 React 开发中最常踩的坑,看到代码能识别出问题,知道怎么修。
1. 直接修改 state
React 的 state 是不可变的。直接改对象或数组,React 不会触发重渲染。
// ❌ 直接修改
function addTodo() {
todos.push(newTodo); // 改了原数组
setTodos(todos); // 引用没变,React 不渲染
}
// ✅ 创建新引用
function addTodo() {
setTodos([...todos, newTodo]); // 新数组
}
// ❌ 直接修改对象属性
user.name = 'Bob';
setUser(user); // 引用没变
// ✅ 创建新对象
setUser({ ...user, name: 'Bob' });
Warning
useState、useReducer的 setter 不会合并对象。要手动展开。
2. 用数组 index 做 key
列表的 key 必须稳定且唯一。用 index 做 key,在增删排序时会出问题。
// ❌ 用 index 做 key
{todos.map((todo, index) => (
<TodoItem key={index} todo={todo} />
))}
// ✅ 用唯一且稳定的 id
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
React 用 key 判断”哪个元素对应哪份数据”。index 在数据重排后会错位。
Note没有 id 的情况下,可以用
crypto.randomUUID()在创建时生成。不要用Math.random()——每次渲染都变。
3. 在 render 里创建新对象/函数
React.memo 和 PureComponent 靠浅比较判断要不要重渲染。
render 里写 options={[]} 或 onClick={() => {}},每次都是新引用,memo 失效。
// ❌ 每次渲染都创建新的数组
function Table({ items }) {
return items.map(i => (
<Cell data={i} options={[]} /> // 新数组!
));
}
// ✅ 提到组件外面
const EMPTY_OPTIONS = [];
function Table({ items }) {
return items.map(i => (
<Cell data={i} options={EMPTY_OPTIONS} />
));
}
TipReact 19 的 Compiler 能自动处理这个问题。但老项目或没开 Compiler 的还是要手动注意。
4. useEffect 依赖数组写错
依赖数组是 Effect 重新执行的触发条件。写少了会读到旧值,写多了会无限循环。
// ❌ 依赖缺失——闭包捕获旧值
useEffect(() => {
console.log(count); // 永远是 0
}, []);
// ✅ 依赖完整
useEffect(() => {
console.log(count);
}, [count]);
// ❌ 依赖对象——每次渲染都是新引用,无限循环
useEffect(() => {
fetchData(config);
}, [config]); // config 是对象,每次渲染新建
// ✅ 依赖原始值
useEffect(() => {
fetchData(config);
}, [config.url, config.method]);
5. 闭包陷阱(过期闭包)
在异步回调里读 state,可能读到的是旧值。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
// ❌ 这里的 count 永远是点击时的值
setCount(count + 1);
}, 1000);
}
// ✅ 用函数式更新,拿到最新值
function handleClick() {
setTimeout(() => {
setCount(prev => prev + 1);
}, 1000);
}
}
Note函数式更新
setState(prev => ...)是解决过期闭包的标准方案。
6. props 透传过深(props drilling)
A → B → C → D,中间层不需要这个 prop,但为了传给 D 必须层层传递。
// ❌ 中间组件被迫接收和传递 theme
function App() {
return <B theme="dark" />;
}
function B({ theme }) {
return <C theme={theme} />;
}
function C({ theme }) {
return <D theme={theme} />;
}
function D({ theme }) {
return <div className={theme}>Hello</div>;
}
// ✅ 用 Context
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<B />
</ThemeContext.Provider>
);
}
function D() {
const theme = useContext(ThemeContext);
return <div className={theme}>Hello</div>;
}
7. 在条件里调用 Hook
Hook 的调用顺序必须稳定。条件调用会破坏这个规则。
// ❌ 条件调用 Hook
function Component({ isAdmin }) {
if (!isAdmin) return null;
const [data, setData] = useState(null); // 有时有,有时没有
// ...
}
// ✅ 提前返回放在 Hook 之后
function Component({ isAdmin }) {
const [data, setData] = useState(null);
if (!isAdmin) return null;
// ...
}
WarningReact 19 的
useHook 可以在条件里调用。但其他 Hook(useState、useEffect 等)不行。
8. 给 DOM 传未知属性
把组件的 props 直接展开到 DOM 上,可能把非标准属性传给浏览器。
// ❌ flag 不是合法 HTML 属性
function Spread(props) {
return <div {...props}>Test</div>;
}
<Spread flag={true} className="content" />
// ✅ 解构出非 DOM 属性
function Spread({ flag, ...domProps }) {
return <div {...domProps}>Test</div>;
}
9. 在 JSX 里写复杂三元
嵌套三元可读性极差。
// ❌ 三层嵌套三元
<div>
{isLoading
? <Spinner />
: isError
? <Error />
: isSuccess
? <Content />
: <Empty />
}
</div>
// ✅ 提前返回
function renderContent() {
if (isLoading) return <Spinner />;
if (isError) return <Error />;
if (isSuccess) return <Content />;
return <Empty />;
}
<div>{renderContent()}</div>
10. 忘记清理 Effect
Effect 里开了定时器、订阅、事件监听,不清理会内存泄漏。
// ❌ 没有清理
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
window.addEventListener('resize', handleResize);
const subscription = eventBus.subscribe(handleEvent);
}, []);
// ✅ 返回清理函数
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
window.addEventListener('resize', handleResize);
const subscription = eventBus.subscribe(handleEvent);
return () => {
clearInterval(timer);
window.removeEventListener('resize', handleResize);
subscription.unsubscribe();
};
}, []);
11. 在 render 里做昂贵计算
每次渲染都跑一遍 O(n^2) 的算法,卡死没商量。
// ❌ 每次渲染都排序
function List({ items }) {
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
return <ul>{sorted.map(...)}</ul>;
}
// ✅ 用 useMemo 缓存
function List({ items }) {
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return <ul>{sorted.map(...)}</ul>;
}
NoteReact 19 的 Compiler 会自动给这种计算加 memo。但理解原理仍然重要。
12. 滥用 useEffect
很多场景不需要 Effect。能用派生 state 就算出来,别存 state + effect 同步。
// ❌ 用 Effect 同步 state
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 直接派生
const fullName = `${firstName} ${lastName}`;
Tip问自己:这个值能不能从现有 state/props 算出来?能的话就不用 Effect。
小结
| 反模式 | 解决方案 |
|---|---|
| 直接改 state | 创建新对象/数组 |
| index 做 key | 用唯一稳定 id |
| render 里创建引用 | 提到外部或 useMemo |
| 依赖数组写错 | 用 ESLint 插件检查 |
| 过期闭包 | 函数式更新 |
| props 透传过深 | Context |
| 条件调用 Hook | 提前返回放 Hook 后 |
| 复杂三元 | 提前返回或子函数 |
| 忘记清理 Effect | 返回清理函数 |
| render 里昂贵计算 | useMemo 或 Compiler |
| 滥用 useEffect | 优先派生 state |