首页 / React 19 入门教程 / 常见反模式与坑

React 19 入门教程

常见反模式与坑

本教程共 50 篇 · 第 49 篇 · 更新于 2026-07-29 · 约 9 分钟阅读

React反模式最佳实践常见错误调试

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

useStateuseReducer 的 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.memoPureComponent 靠浅比较判断要不要重渲染。

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} />
  ));
}
Tip

React 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;
  // ...
}
Warning

React 19 的 use Hook 可以在条件里调用。但其他 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>;
}
Note

React 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