前言
去年我们团队决定把那个用了5年的老后台管理系统从 jQuery 迁移到 React。项目不大,但模块挺多,而且业务逻辑混杂着大量 DOM 操作。我以为这活两个月就能搞定,结果前后折腾了大半年。今天就来聊聊我们踩过的那些坑,希望能给想迁移的朋友一点参考。

第一个坑:先别急着开干,梳理边界
我们上来就打算“全面重写”,结果发现根本行不通。代码里到处都是 $(document).ready、$.ajax、$(selector).on('click', ...),而且很多功能是相互依赖的。我建议先做“边界分析”,把纯数据逻辑和 DOM 操作分开。比如,如果你的页面上有一个列表,点击加载更多,那列表的数据获取和渲染完全可以抽象成一个组件;但如果你用 jQuery 做了很多直接操作 DOM 树(比如动态插入表格行),那就得考虑是否真的需要 React 的状态管理。
// jQuery 方式
$('#load-more').click(function() {
$.get('/api/list', function(data) {
data.forEach(item => {
$('#table').append(`<tr><td>${item.name}</td></tr>`);
});
});
});迁移后应该变成:
function List() {
const [items, setItems] = useState([]);
const loadMore = () => {
fetch('/api/list')
.then(res => res.json())
.then(data => setItems(prev => [...prev, ...data]));
};
return (
<table>
{items.map(item => <tr key={item.id}><td>{item.name}</td></tr>)}
</table>
);
}如果直接一股脑重写,很容易陷入“为了用 React 而用 React”的陷阱,最后连 npm 包都选了一大堆没必要的。
第二个坑:jQuery 插件怎么救?
很多老项目依赖了大量 jQuery 插件:比如 datepicker、select2、datatables。这些插件直接操作 DOM,放到 React 里会出各种问题:插件初始化时拿不到正确的 DOM 节点、React 重新渲染时插件状态丢失、事件冲突。
我们的办法是:用 ref 包裹插件。比如一个日期选择器:
import $ from 'jquery'; // 别急着删,有时候还得用
function DatePicker({ value, onChange }) {
const inputRef = useRef(null);
useEffect(() => {
$(inputRef.current).datepicker({
dateFormat: 'yy-mm-dd',
onSelect: (dateText) => onChange(dateText)
});
return () => $(inputRef.current).datepicker('destroy');
}, []);
return <input ref={inputRef} defaultValue={value} />;
}这方法能临时过渡,但长期看最好还是换 React 原生组件库。我们最后把所有插件都替换成了 antd 或者自定义组件,虽然费时但值得。

第三个坑:事件委托和异步 DOM
jQuery 里大量使用事件委托,比如 $(document).on('click', '.dynamic-btn', handler)。React 不鼓励这么做。迁移时如果直接改成在动态生成的 JSX 里绑定 onClick,会碰到因为异步加载导致 handler 里使用了过时的闭包变量。
比如,列表里有一个“删除”按钮,点击后需要知道当前行的 id。在 jQuery 中,我们可能通过 data-*属性获取;在 React 中,如果用箭头函数 onClick={() => handleDelete(id)},如果 id 是循环变量,那你得小心闭包陷阱。
// 错误写法:id 是旧的
{items.map(item => (
<button onClick={() => handleDelete(item.id)}>删除</button>
))}看起来没问题?但如果 items 在异步操作后被替换,handleDelete 里的 item 可能还是旧的。正确做法是把 id 作为一个参数传递:
function handleDelete(id) {
// 使用 id 进行操作
}
// 然后在 JSX 中用 bind 或者闭包都可以,但注意 key 要稳定还有个坑:在 useEffect 里绑定的事件,如果不清除,会导致内存泄漏。我们团队有人因为忘了 cleanup,结果路由切换后旧组件还在监听滚动事件,页面性能雪崩。
第四个坑:全局状态和第三方库的相爱相杀
jQuery 项目里很多状态存在全局变量或者 DOM 属性里(比如 $(el).data('xx'))。迁移到 React 后,大家第一反应就是上 Redux 或 Mobx。其实对于中小项目,useContext + useReducer 就够了,别为了“架构”过度设计。
我们当时给每个页面都创建了独立的 store,结果跨页面通信时一堆 window.eventBus 又回来了,简直是开倒车。后来才明白:React 的状态管理只应该管理 UI 相关的状态,业务数据的状态尽量用后端接口保障。
还有一个坑是第三方库的异步操作。比如我们用了一个 WebSocket 库,它直接修改 DOM 来显示消息,和 React 的虚拟 DOM 打架。解决办法是让 WebSocket 只触发 React 的事件,然后由 React 自己更新视图。
useEffect(() => {
const socket = new WebSocket('wss://...');
socket.onmessage = (event) => {
// 不要直接操作 DOM,而是 dispatch
dispatch({ type: 'NEW_MESSAGE', payload: event.data });
};
return () => socket.close();
}, []);第五个坑:性能——React 的“无脑重渲染”
jQuery 追求“精准操作”,React 默认是“全量 diff”。刚开始迁移后,我们发现页面卡顿得厉害。原因是组件层级太深,每次 state 变化太多组件重渲染。后来添加了 React.memo、useMemo、useCallback,并配合 React Developer Tools 排查,才逐步优化。
其中一个典型问题:列表中的每个 li 都包含一个复杂子组件,这个子组件订阅了全局状态。当全局状态改变时,所有 li 都重新渲染。解决办法是让子组件通过 props 只接收它需要的数据,并且使用 React.memo 浅比较。
const ListItem = React.memo(({ item, onDelete }) => {
return (
<li>
<span>{item.name}</span>
<button onClick={() => onDelete(item.id)}>删除</button>
</li>
);
});另外,事件处理函数的绑定也很关键。如果每次渲染都创建新的函数,子组件的 React.memo 就失效了,所以最好用 useCallback。
最后的碎碎念
迁移过程极度痛苦,尤其是一边维护老代码一边翻新。但回头看看,代码的可维护性和开发效率确实提升了一大截。React 不是银弹,但 jQuery 也不是。如果你的项目很复杂,迁移前一定做好技术债务的清理,别指望 React 能帮你自动解决设计问题。

对了,还有一件事:如果你决定迁移,先把测试覆盖率提上来,否则你会怀疑人生。
觉得内容不错?我要