当 React 开始像 jQuery 那样行事(而且你完全能看出来 😅)
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-JS、React 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>
);
}
为什么这些写法会让人“一眼就看出”?
- 直接使用
$选择器:在 React 项目里出现$(或jQuery)的引用,几乎是“红灯”。它暗示着开发者在尝试手动控制 DOM,而不是让 React 来完成这件事。 - 在生命周期钩子里写副作用:如果副作用是 DOM 操作 而不是 数据获取,这通常意味着我们在用 React 之外的方式来管理 UI。
- 混合两套状态系统:React 有自己的状态(
state、useState、useReducer),而 jQuery 通过直接修改 DOM 来维护 UI 状态。两者并存会导致状态不一致。 - 代码可读性下降:当团队成员看到
$、.hide()、.show()等 jQuery 方法时,往往会立刻怀疑该组件是否遵循了 React 的最佳实践。
如何彻底摆脱“React 像 jQuery” 的陷阱?
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | 移除所有 import $ from 'jquery' | 确保项目不再依赖 jQuery。 |
| 2 | 把所有 DOM 操作迁移到 React 状态或 refs | 让 UI 完全受 React 控制。 |
| 3 | 使用 CSS 或 React 动画库(如 react-spring、framer-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"));
}, []);
原文中省略了后续代码。