当 React 开始像 jQuery 那样行事(而且你完全能看出来 😅)

发布: (2026年1月8日 GMT+8 21:00)
16 min read
原文: Dev.to

Source: Dev.to

当 React 开始像 jQuery 那样行事,你完全能看出来

在使用 React 时,最常见的错误之一就是把它当成普通的 DOM 操作库来使用——也就是把 jQuery 的思维方式直接搬进了 React 组件里。下面的示例展示了几种典型的“React + jQuery”写法,以及为什么它们会让你的代码变得难以维护、难以调试,甚至会导致意想不到的 bug。


例子一:在 componentDidMount 中直接操作 DOM

class MyComponent extends React.Component {
  componentDidMount() {
    // ❌ 直接使用 jQuery 操作 DOM
    $('.my-element').hide();
  }

  render() {
    return <div className="my-element">Hello world</div>;
  }
}

问题

  • 违背了 单向数据流 的原则。React 负责渲染 UI,手动隐藏元素会让 React 对实际的 UI 状态失去控制。
  • 当组件重新渲染时,React 会重新把元素插入 DOM,导致隐藏状态被“重置”。
  • 代码难以复用:如果以后想把这个组件抽离到另一个项目,所有的 jQuery 依赖都必须一起搬过去。

推荐做法
使用 React 的状态来控制可见性:

class MyComponent extends React.Component {
  state = { isVisible: false };

  componentDidMount() {
    this.setState({ isVisible: true });
  }

  render() {
    return this.state.isVisible ? (
      <div className="my-element">Hello world</div>
    ) : null;
  }
}

例子二:在事件处理函数里使用 $ 选择器

function SearchBox() {
  const handleSearch = () => {
    const query = $('#search-input').val(); // ❌ 直接读取 DOM
    // 进行搜索...
  };

  return (
    <div>
      <input id="search-input" type="text" />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

问题

  • 通过 ID 直接抓取元素会导致 紧耦合,一旦 ID 改变或组件被多次实例化(比如在列表中渲染),代码就会失效。
  • 失去了 受控组件 的优势——React 无法追踪输入框的值,也就无法在需要时进行表单校验或自动填充。

推荐做法
使用受控组件或 ref

function SearchBox() {
  const [query, setQuery] = React.useState('');
  const inputRef = React.useRef(null);

  const handleSearch = () => {
    // 直接使用 state 或 ref
    console.log(query);
    // 或者: console.log(inputRef.current.value);
  };

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <button onClick={handleSearch}>Search</button>
    </div>
  );
}

例子三:在 useEffect 中混用 jQuery 动画

function FadeInBox() {
  React.useEffect(() => {
    // ❌ 用 jQuery 的 fadeIn/fadeOut
    $('.box').fadeIn(500);
  }, []);

  return <div className="box">I will fade in</div>;
}

问题

  • jQuery 动画会直接操作元素的 style,而 React 也会在每次渲染时重新计算这些 style,可能导致动画被意外中断。
  • 这种写法让 CSS-in-JSReact Transition Group 等更现代、更可组合的方案失去意义。

推荐做法
使用 CSS 动画或 React 生态中的动画库:

import { CSSTransition } from 'react-transition-group';
import './fade.css'; // 包含 .fade-enter、.fade-enter-active 等类

function FadeInBox() {
  const [show, setShow] = React.useState(false);

  React.useEffect(() => {
    setShow(true);
  }, []);

  return (
    <CSSTransition in={show} timeout={500} classNames="fade" unmountOnExit>
      <div className="box">I will fade in</div>
    </CSSTransition>
  );
}

为什么这些写法会让人“一眼就看出”?

  1. 直接使用 $ 选择器:在 React 项目里出现 $(或 jQuery)的引用,几乎是“红灯”。它暗示着开发者在尝试手动控制 DOM,而不是让 React 来完成这件事。
  2. 在生命周期钩子里写副作用:如果副作用是 DOM 操作 而不是 数据获取,这通常意味着我们在用 React 之外的方式来管理 UI。
  3. 混合两套状态系统:React 有自己的状态(stateuseStateuseReducer),而 jQuery 通过直接修改 DOM 来维护 UI 状态。两者并存会导致状态不一致。
  4. 代码可读性下降:当团队成员看到 $.hide().show() 等 jQuery 方法时,往往会立刻怀疑该组件是否遵循了 React 的最佳实践。

如何彻底摆脱“React 像 jQuery” 的陷阱?

步骤操作目的
1移除所有 import $ from 'jquery'确保项目不再依赖 jQuery。
2把所有 DOM 操作迁移到 React 状态或 refs让 UI 完全受 React 控制。
3使用 CSS 或 React 动画库(如 react-springframer-motion替代 jQuery 的动画方法。
4审查第三方库,确保它们不内部使用 jQuery。防止隐式依赖。
5写单元测试,验证 UI 只通过 props/state 变化来更新。防止以后再次出现手动 DOM 操作。

小结

React 的核心价值在于 声明式 UI单向数据流。一旦我们把它当成传统的 DOM 操作工具(比如 jQuery),就会失去这些优势,代码也会变得难以维护。只要遵循以下原则:

  • 不要在组件内部直接使用 $jQuery
  • 所有 UI 状态都放在 React 的 state/props 中
  • 使用 React 生态系统提供的动画和副作用解决方案

就能让你的 React 项目保持干净、可预测且易于扩展。祝编码愉快!

不是粉丝——只是工具使用者

我并不是任何单一框架的粉丝。对我来说,框架只是工具。
就像我宁愿使用一把结实、坚固的锤子,而不是那把生锈、打洞的锤子一样,我也更倾向于使用能够帮助我构建可维护、使用愉快的软件的工具。这也是为什么我喜欢保持最新,了解底层是如何运作的,而不是仅仅 “使用大家都在用的东西。”

我每天都在使用 Angular —— 是的,Angular 会把你逼入一种特定的架构(不过相信我,你仍然 可以 在其中写出彻头彻尾的意大利面代码……问问我是怎么知道的 😬)。

但我真的很爱 React。每当我做副项目时,我经常会选它。我喜欢 JSX,也喜欢 React 与原生 JavaScript 的亲近感,尤其是相较于 Angular。不过……你也知道人们常说:能力越大,责任越大 😎。在错误的手中,这种自由会把项目烧毁。

我见过不少项目不仅仅是乱糟糟的。它们看起来像是有人先用 jQuery 写好,然后删掉了 $(document).ready,再撒上一点 JSX … 并且真的想让它跑起来 🤡。

  • 这是一场盲目的旧代码库迁移吗?
  • 还是那些在内心深处从未真正摆脱 jQuery 的开发者所为? 😉

很难说。无论如何——下面是一些 经典迹象,表明你的 React 代码根本不是 React……它只是穿着 JSX 的 jQuery。而且,的确,它 表现 得很明显——响亮地。


1️⃣ 一个“包罗万象”的巨型组件

一个 React 文件(通常是 .tsx),技术上可能包含子组件或用于 CSS 类名的工具函数,但在感受上就像是 老式的 index.html,里面只有一个巨大的 <div> 标签 😂。

大块头:获取数据、更新 UI、管理事件、处理布局,甚至可能还有模态框、三个下拉菜单、一个表格、一个侧边栏……因为为什么不呢?

🧨 示例

function App() {
  const [data, setData] = useState([]);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [filter, setFilter] = useState("");

  useEffect(() => {
    fetch("/api/items")
      .then(res => res.json())
      .then(setData);

    // Direct DOM manipulation – a red flag
    document.getElementById("loader")?.style.display = "none";
    document.getElementById("list")?.style.display = "block";

    const el = document.getElementById("filter");
    el?.addEventListener("input", e => setFilter(e.target.value));
  }, []);

  return (
    <>
      {/* Loading... */}
      {data
        .filter(d => d.includes(filter))
        .map(d => (
          <div key={d}>- {d}</div>
        ))}

      <button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
      {sidebarOpen && "Hello"}
    </>
  );
}

🤔 为什么会出现这种情况?

  • 从 jQuery 迁移过来后把所有东西都塞进了一个文件。
  • “能跑就行,别动它”。
  • 没有提前做架构决策。
  • “以后再重构”(我们都知道这个故事的结局)。

✅ 正确的做法

  • 按职责拆分组件(单一职责原则)。
    • 数据/逻辑组件。
    • 展示组件。
  • 在合适的情况下将可复用的逻辑抽取到 自定义 Hook
  • 避免直接操作 DOM——让 React 来管理 UI。
  • 记住:大量小组件 > 一个神组件

2️⃣ 一个巨大的 useEffect 做…所有事

useEffect(() => {
  // 🤹‍♂️ 一切都在这里发生
}, []);

获取数据、添加事件监听器、切换类名、更新 DOM、与多个服务交互、滚动、分析、弹出通知…全部集中在一个辉煌的 effect 中。

基本上就是:$(document).ready 换成 useEffect,直接上线 😎。

🧨 示例

useEffect(() => {
  fetch("/api/stats")
    .then(res => res.json())
    .then(data => {
      setStats(data);
      document.title = "Dashboard";
      const counter = document.getElementById("counter");
      if (counter) {
        counter.textContent = data.users;
      }
    });

  const resize = () => {
    document.body.classList.toggle("mobile", window.innerWidth < 768);
  };
  window.addEventListener("resize", resize);
  return () => window.removeEventListener("resize", resize);
}, []);

🤔 为什么会出现这种情况?

  • 缺乏思维模型:“effect = 魔法发生的地方”。
  • 从旧代码中复制‑粘贴。
  • “嘿,它只运行一次,正好可以放所有东西!”
  • 没有理解 effect 应该是聚焦且有范围的。

✅ 正确的做法

  • 每个 effect 应该只有一个明确的职责
  • 将巨大的 useEffect 拆分为多个聚焦的 effect。
  • 如果逻辑可以放在渲染阶段,就不要放在 effect 中。
  • 记住:useEffect !== 生命周期方法
  • 避免把所有应用行为塞进一个“只运行一次”的 effect。

3️⃣ useEffect 用于本该在 JSX 中的事情

这点有点让人头疼 😅。在 effect 中进行 DOM 操作,只是为了更新文本、类名、可见性,或者其他 JSX 本可以自然声明的内容。

而不是

if (condition) {
  // change DOM
}

React 想要的是

if (condition) {
  // render something else
}

🧨 示例(命令式)

useEffect(() => {
  const el = document.getElementById("message");
  if (error) {
    el?.classList.add("visible");
    el!.textContent = error;
  } else {
    el?.classList.remove("visible");
  }
}, [error]);

✅ 声明式 JSX 版本

{error && <div className="visible">{error}</div>}

🤔 为什么会这样?

  • 仍然以命令式思维:“UI 是我 改变 的,而不是我 描述 的”。
  • 旧的模式:“所有动态的东西?→ 必须放进 useEffect!”
  • 旧习惯难以改掉。

✅ 正确的做法

  • 在 JSX 中以声明式方式表达 UI 变化
  • 使用条件渲染来显示/隐藏元素。
  • 根据状态派生类名,而不是手动切换类名。
  • 思考:“状态变化 → React 重新渲染” 而不是 “状态变化 → 我去补丁 DOM”。

TL;DR

  • 保持组件小且专注。
  • 使用自定义 Hook 来实现可复用的逻辑。
  • useEffect 仅用于副作用 only(数据获取、订阅等)。
  • 让 JSX 描述 UI,避免手动操作 DOM。

当你遵循这些指南时,你的 React 代码会重新回到 React 的感觉——不再像 jQuery 风格的 Frankenstein。 🚀

“the DOM”

4️⃣ 用 CSS 类的巨型“开关”代替真实逻辑

UI 状态存储…不在 state 中
…也不在 props 中
…而是 🥁 在 CSS 类名里

应用逻辑变成:

  • “如果有这个类则是打开状态”
  • “如果没有则是关闭状态”
  • “如果同时拥有这个和那个类,则进入某种没人完全理解的神奇状态”

恭喜,你在样式表里构建了一个状态机 🙃

🧨 示例

useEffect(() => {
  const steps = document.querySelectorAll(".step");
  steps.forEach(step => {
    step.addEventListener("click", () => {
      steps.forEach(s => s.classList.remove("active"));
      step.classList.add("active");
    });
  });
}, []);
<button class="step">Enter fullscreen mode</button>
<button class="step">Exit fullscreen mode</button>

🤔 为什么会出现这种情况?

  • 传统思维。
  • “这在 jQuery 里以前还能用,为什么要改?”
  • 多年来 CSS 被当作状态持有者——习惯难改。

✅ 正确的做法

  • 将 UI 状态保存在 React state 或 store 中。
  • 让 UI 根据状态渲染,而不是把逻辑写进 CSS。

对于复杂流程可以考虑:

  • useReducer
  • 正规的状态机

经验法则

  • CSS = 样式
  • React state = 逻辑

5️⃣ 动画仅仅是“在 JS 中切换类”

需要动画?
在 JS 中添加/移除类。完成。

React?哦,是的,技术上仍然在 👀 静静地观察。

没有状态。没有声明式过渡。没有结构。

只有:

click → add class → remove class later → hope nothing breaks

🧨 示例

function Notification() {
  useEffect(() => {
    const btn = document.getElementById("show");
    const box = document.getElementById("note");

    btn!.onclick = () => {
      box?.classList.add("visible");
      setTimeout(() => box?.classList.remove("visible"), 2000);
    };
  }, []);

  return (
    <>
      <button id="show">Show</button>
      <div id="note">Hello!</div>
    </>
  );
}
<button id="show">Enter fullscreen mode</button>
<button id="hide">Exit fullscreen mode</button>

🤔 为什么会出现这种情况?

  • 熟悉的老模式。
  • 快速 hack 直接上线。
  • 前端架构没有预留时间。

✅ 正确的做法

  • 基础动画应基于 React 状态,而不是手动操作 DOM。

最简方案

  • 状态 → JSX 中的类切换

更佳方案

  • 由状态驱动的 CSS 过渡
  • React Transition Group
  • Framer Motion

当可以使用声明式方式时,避免以命令式方式驱动动画。

6️⃣ 将 DOM 元素保存在 state 中 😱

很少见…但我见过。一旦看到,就很难忘记——它会留下印记 😅

const [el, setEl] = useState<HTMLElement | null>(null);

useEffect(() => {
  setEl(document.getElementById("target"));
}, []);

原文中省略了后续代码。

Back to Blog

相关文章

阅读更多 »

React 是什么?

封面图片:What is React? https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amaz...