はじめに
jQueryからReactへの移行に関わっていると、最初に困るのはJSXの書き方そのものではありません。
$('.button').on('click', ...) はReactで何になるのか。addClass() はどうするのか。$.ajax() はfetch()に変えれば終わりなのか。こうした「いま目の前にある処理を、Reactではどう考えるのか」という対応関係が分からないと、移行作業はなかなか前へ進みません。
私自身、jQueryからReactへの移行や古いコードの作り直しに関わる中で、単純なAPI置換では整理しにくいと感じてきました。
この記事では、jQueryでよく見かける処理を題材に、Reactへ持っていくときの対比をコード付きで整理します。最後に、そのまま移行時のレビューに使えるチェックリストも載せます。
結論
jQueryからReactへの移行では、APIを一対一で置き換えないほうが整理しやすいです。
「DOMを変更する処理」を「stateからUIを作る処理」へ読み替えます。
直接DOMを触るコードは、必要な部分だけrefやEffectの境界へ残します。
環境
この記事のコードは、次の構成を前提にしています。
OS: macOS / Windows / Linux
Language: JavaScript (ES2022以降を想定)
React: 19.3.0
React DOM: 19.3.0
Vite: 8.3.1
jQuery: 3.7.1(移行元コードの例)
Browser: ES Modules / Fetch APIを利用できるモダンブラウザ
2026年9月27日時点でnpm上のReactのlatestは19.3.0、Viteのlatestは8.3.1です。jQueryは4.0.0がlatestですが、この記事では既存資産からの移行を想定し、移行元の例を3.7.1として書いています。
なお、React 19.3固有の機能を積極的に使う記事ではありません。useState、useEffect、useRefなど、移行時に基本となる仕組みに絞ります。
実装
まず「置換表」を作らない
最初に整理しておきたいのが、jQueryとReactの発想の違いです。
たとえば、次のjQueryは自然です。
$('#save-button').on('click', function () {
$('#message').text('保存しました');
$('#message').addClass('success');
});
このコードでは、
- ボタンを探す
- イベントを登録する
- メッセージ要素を探す
- テキストを書き換える
- classを書き換える
という順番でDOMを操作しています。
Reactへ移すとき、これを
$() -> ???
.on() -> ???
.text() -> ???
.addClass() -> ???
という表にすると、途中から苦しくなります。
Reactでは、表示したい状態を先に持ちます。
import { useState } from 'react';
export default function SaveButton() {
const [saved, setSaved] = useState(false);
return (
<div>
<button onClick={() => setSaved(true)}>
保存
</button>
{saved && (
<p className="success">
保存しました
</p>
)}
</div>
);
}
ここでは「メッセージ要素を書き換える」という操作が消えています。
あるのは、
setSaved(true);
だけです。
trueなら成功メッセージを描画する。falseなら描画しない。この関係をJSXで宣言します。
この分類を先にすると、移行後のコードがかなり読みやすくなります。
クリックイベント:.on('click') → onClick
jQueryでは、あとからDOMへイベントを登録する書き方をよく使います。
$('#open-button').on('click', function () {
$('#panel').show();
});
ReactではイベントハンドラをJSXへ書きます。
import { useState } from 'react';
export default function Panel() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>
開く
</button>
{open && (
<div className="panel">
パネルの内容
</div>
)}
</>
);
}
ポイントは、.on()をonClickへ変えただけではないことです。
jQuery版の主語は「#panelを表示する」です。
React版の主語は「openをtrueにする」です。
UIはその結果として変わります。
閉じる処理も同じです。
<button onClick={() => setOpen(false)}>
閉じる
</button>
トグルならこうなります。
<button onClick={() => setOpen((current) => !current)}>
{open ? '閉じる' : '開く'}
</button>
この形まで持っていけると、DOM操作を探し回る必要がなくなります。
.show() / .hide() → 条件付きレンダリング
既存コードでは、この組み合わせもよく見ます。
if (hasError) {
$('#error-message').show();
} else {
$('#error-message').hide();
}
Reactでは、要素の存在自体を条件で決められます。
{hasError && (
<p className="error-message">
入力内容を確認してください
</p>
)}
DOMには残してCSSだけ切り替えたいなら、classをstateから決めます。
<div className={hasError ? 'error-message visible' : 'error-message'}>
入力内容を確認してください
</div>
どちらを選ぶかは要件次第です。
単に見えなくなればよいのか、アニメーションのためDOMを残したいのか、アクセシビリティ上どう扱うのか。このあたりは「jQueryでhide()だったから」という理由では決めません。
移行では、元コードの命令より元の画面が何を実現しようとしているかを見るようにしています。
.addClass() / .removeClass() → stateからclassName
たとえば選択行を強調するコードです。
$('.row').on('click', function () {
$('.row').removeClass('selected');
$(this).addClass('selected');
});
Reactでは「どの行が選ばれているか」をstateにします。
import { useState } from 'react';
const items = [
{ id: 'a', name: '商品A' },
{ id: 'b', name: '商品B' },
{ id: 'c', name: '商品C' },
];
export default function ItemList() {
const [selectedId, setSelectedId] = useState(null);
return (
<ul>
{items.map((item) => (
<li
key={item.id}
className={selectedId === item.id ? 'row selected' : 'row'}
onClick={() => setSelectedId(item.id)}
>
{item.name}
</li>
))}
</ul>
);
}
jQuery版では、
$('.row').removeClass('selected');
$(this).addClass('selected');
というDOMの変更そのものが状態を表しています。
React版では、
selectedId
が状態です。
この違いはかなり重要です。
DOMを見なければ現在の選択状態が分からない設計から、JavaScriptの値を見れば分かる設計へ変わります。
.val() → controlled input
フォームも移行時に整理しやすい場所です。
jQueryなら、送信時に値を取り出すコードがよくあります。
$('#save-button').on('click', function () {
const name = $('#name').val();
const email = $('#email').val();
save({
name,
email,
});
});
Reactではcontrolled inputとしてstateへ持てます。
import { useState } from 'react';
export default function UserForm() {
const [form, setForm] = useState({
name: '',
email: '',
});
function updateField(event) {
const { name, value } = event.target;
setForm((current) => ({
...current,
[name]: value,
}));
}
function handleSubmit(event) {
event.preventDefault();
save(form);
}
return (
<form onSubmit={handleSubmit}>
<label>
名前
<input
name="name"
value={form.name}
onChange={updateField}
/>
</label>
<label>
メールアドレス
<input
name="email"
type="email"
value={form.email}
onChange={updateField}
/>
</label>
<button type="submit">
保存
</button>
</form>
);
}
これも単純に、
.val() -> useState()
と覚えるより、「フォームの値をDOMだけに持たせるか、Reactのstateとして管理するか」と考えるほうが分かりやすいです。
ただし、すべてのinputを必ずcontrolledにする必要があるわけではありません。
既存フォームが大きく、一気にstate化すると変更範囲が広がる場合もあります。移行では、画面単位、フォーム単位で境界を決めたほうが安全です。
.html()で画面を組み立てる処理 → JSX
jQueryの古い画面では、HTML文字列を組み立てて差し込むコードもあります。
function renderUser(user) {
$('#user').html(`
<div class="user-card">
<span class="user-name">${user.name}</span>
<button class="edit-button">編集</button>
</div>
`);
}
Reactでは、その構造をコンポーネントとして書けます。
function UserCard({ user, onEdit }) {
return (
<div className="user-card">
<span className="user-name">
{user.name}
</span>
<button onClick={() => onEdit(user.id)}>
編集
</button>
</div>
);
}
HTML文字列を生成してからイベントを再登録する必要もありません。
さらに、ReactのJSXで文字列を表示するときは通常エスケープされます。既存コードにユーザー入力を含むHTML文字列の連結がある場合は、移行時にその扱いも確認します。
dangerouslySetInnerHTMLを使えばHTMLを直接挿入できますが、「元が.html()だから」という理由だけで移植するのは避けています。
HTMLとして解釈する必要が本当にあるのかを先に確認します。
$.ajax() → fetch()、ただし通信と表示状態を分ける
jQueryでは通信も画面操作と一緒になりやすいです。
$('#search-button').on('click', function () {
$('#loading').show();
$.ajax({
url: '/api/users',
method: 'GET',
data: {
keyword: $('#keyword').val(),
},
})
.done(function (users) {
renderUsers(users);
})
.fail(function () {
$('#error').show();
})
.always(function () {
$('#loading').hide();
});
});
Reactへ移すときは、通信方法より先に状態を分解します。
必要なのは、たとえば次の値です。
keyword
users
loading
error
実装するとこうなります。
import { useState } from 'react';
export default function UserSearch() {
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function searchUsers() {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({
keyword,
});
const response = await fetch(`/api/users?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
return (
<section>
<input
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
<button
onClick={searchUsers}
disabled={loading}
>
検索
</button>
{loading && <p>読み込み中...</p>}
{error && (
<p role="alert">
データを取得できませんでした。
</p>
)}
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
</section>
);
}
ここで注意したいのは、$.ajax()とfetch()には挙動の違いがあることです。
特にfetch()はHTTP 404や500を受け取っただけではPromiseをrejectしません。そのため、必要ならresponse.okなどを確認します。
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
単なるAPI名の変更で済ませず、既存コードが暗黙に期待していたエラー処理まで確認する必要があります。
$(document).ready() → まず本当にEffectが必要か考える
jQueryでは初期化処理をまとめるために、次のようなコードがあります。
$(function () {
initializeForm();
loadInitialData();
bindEvents();
});
Reactを始めた直後だと、これを全部useEffect()へ入れたくなります。
useEffect(() => {
initializeForm();
loadInitialData();
bindEvents();
}, []);
私はこの機械的な移植をしないようにしています。
bindEvents()は、Reactのイベントハンドラへ移せる可能性があります。
initializeForm()も、単に初期値を設定しているならuseState()の初期値で表現できるかもしれません。
const [form, setForm] = useState({
name: '',
category: 'general',
});
useEffectは「コンポーネントが表示されたら何でも実行する場所」ではありません。
Reactの公式ドキュメントでも、Effectは外部システムとの同期に使うものとして説明されています。外部システムとの同期でなければ、Effectが不要なケースがあります。
たとえばブラウザのイベントを購読するなら、Effectが自然です。
import { useEffect, useState } from 'react';
export default function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>width: {width}</p>;
}
登録したイベントをcleanupで解除するところまでセットです。
focus()やスクロールはrefへ
ReactではDOMを一切触ってはいけない、という話でもありません。
たとえばjQueryで、
$('#name').focus();
としていた処理です。
フォーカスはDOMそのものへの命令なので、refが適しています。
import { useRef } from 'react';
export default function Form() {
const nameRef = useRef(null);
function focusName() {
nameRef.current?.focus();
}
return (
<>
<input ref={nameRef} />
<button type="button" onClick={focusName}>
名前欄へ移動
</button>
</>
);
}
スクロールも同じ考え方です。
import { useRef } from 'react';
export default function ErrorNavigation() {
const errorRef = useRef(null);
function moveToError() {
errorRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
return (
<>
<button onClick={moveToError}>
エラーを確認
</button>
<div ref={errorRef}>
入力内容を確認してください。
</div>
</>
);
}
React公式ドキュメントでも、focus、scroll、サイズや位置の測定など、DOMノードへのアクセスが必要な場面でrefを使う方法が示されています。
大事なのは「DOM操作禁止」ではなく、描画状態として表せるものまでrefで操作しないことだと考えています。
.each() → map()、ただしDOMではなくデータを回す
jQueryではDOMを取得してからループするコードがあります。
$('.user-row').each(function () {
const id = $(this).data('id');
console.log(id);
});
Reactでは、DOMを回す前に元データを回せないか確認します。
const users = [
{ id: 'u1', name: '田中' },
{ id: 'u2', name: '鈴木' },
];
export default function UserList() {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
);
}
何か処理したい場合も、
users.forEach((user) => {
console.log(user.id);
});
で済むことがあります。
jQueryではDOMがデータ置き場を兼ねているコードがあります。
<tr
class="user-row"
data-user-id="u1"
data-status="active"
>
そして、
const userId = $('.selected').data('user-id');
のように現在状態を取り出します。
Reactへ移すなら、
const [selectedUserId, setSelectedUserId] = useState(null);
のように、アプリケーション側へ状態を戻せないか考えます。
これもAPI変換というより、データの置き場所を変える作業です。
イベントデリゲーションはどうするか
jQueryで非常に便利なのがイベントデリゲーションです。
$('#user-list').on('click', '.delete-button', function () {
const id = $(this).data('id');
deleteUser(id);
});
あとから追加された要素にも対応できます。
Reactでは、一覧を描画するときにイベントハンドラも一緒に渡せます。
function UserList({ users, onDelete }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
<span>{user.name}</span>
<button onClick={() => onDelete(user.id)}>
削除
</button>
</li>
))}
</ul>
);
}
React側では「あとから追加されたDOMへイベントを登録し直す」という発想自体が不要になる場面が多いです。
usersが変われば再レンダリングされ、新しいボタンにも必要なハンドラが含まれます。
モーダルも「開く命令」から状態へ変える
jQueryプラグインを含む既存画面では、モーダルが移行境界になりやすいです。
単純な自作モーダルなら、
$('#modal').addClass('open');
ではなく、
const [modalOpen, setModalOpen] = useState(false);
として、
{modalOpen && (
<Modal onClose={() => setModalOpen(false)} />
)}
とできます。
一方、既存のモーダルライブラリをすぐ撤去できない場合もあります。
その場合まで無理にReact化する必要はありません。
import { useEffect, useRef } from 'react';
function LegacyWidget({ value }) {
const containerRef = useRef(null);
useEffect(() => {
const element = containerRef.current;
const widget = createLegacyWidget(element, {
value,
});
return () => {
widget.destroy();
};
}, []);
return <div ref={containerRef} />;
}
これは概念を示す例ですが、既存ライブラリをReact管理下のDOMと混ぜるときは「誰がこのDOMを管理するのか」を明確にします。
React公式ドキュメントでも、React外のウィジェットとの同期はEffectの用途として挙げられています。
対比を一枚にするとこうなる
移行時に私が見る対応関係を、簡略化すると次のようになります。
| jQueryで見つけた処理 | React側で最初に考えるもの |
|---|---|
.on('click', ...) |
JSXのイベントハンドラ |
.show() / .hide()
|
state + 条件付きレンダリング |
.addClass() / .removeClass()
|
stateからclassNameを決定 |
.val() |
フォームstate、必要に応じてref |
.text() |
JSX内の値 |
.html() |
JSX / コンポーネント |
.each() |
データのmap() / forEach()
|
$.ajax() |
fetch() + 通信状態 |
$(document).ready() |
初期値、イベント、Effectへ分解 |
.focus() |
useRef() |
| DOMプラグイン初期化 |
useEffect() + cleanup |
data-*を状態保存に使用 |
props / state / データモデル |
この表は「右側へ機械的に変換すればよい」という意味ではありません。
むしろコードを読む順番を決めるための表です。
たとえば.addClass()を見つけたら、「ReactならclassName」と即座に書き換えるのではなく、「このclassは何の状態を表しているのか」を探します。
selectedならselectedIdかもしれません。
loadingならloadingというbooleanかもしれません。
errorならエラーオブジェクトの有無かもしれません。
名前の付いた状態が見つかれば、そこからReact側の設計を始められます。
🔧 段階移行するときの切り方
jQueryの画面をReactへ移すとき、全面的に書き直せるとは限りません。
既存の業務システムでは、古い画面を動かしたまま一部分ずつ変える場面も考える必要があります。
そのときは、セレクタ単位ではなく「画面上の責務」で境界を切るほうが扱いやすいです。
たとえば検索画面なら、
検索条件
検索ボタン
検索結果
詳細モーダル
という単位があります。
このうち検索結果だけReactへ移すなら、jQueryとReactの双方が同じDOMを書き換えないようにします。
<div id="legacy-search-form">
<!-- jQueryが管理 -->
</div>
<div id="react-search-result">
<!-- Reactが管理 -->
</div>
境界を越える値は、関数やイベントなど明示的な方法で渡します。
避けたいのは、
$('.react-area .row').addClass('selected');
のように、Reactが管理している領域へjQuery側から手を伸ばすことです。
逆方向も同じです。
Reactコンポーネントの中から、理由なく既存DOMを探して変更し始めると、どちらが最終状態を決めているのか分からなくなります。
私は移行を考えるとき、
このDOMの所有者は誰か
を決めることを重視しています。
技術的には同じページにjQueryとReactを置けても、同じ要素を双方が管理すると複雑さが急に増えます。
⚠️ ハマりどころ
useEffectをjQuery置き場にしてしまう
移行直後に起こりやすいのが、jQuery時代の処理をそのままEffectへ移す形です。
useEffect(() => {
document.querySelector('#button')
?.addEventListener('click', handleClick);
document.querySelector('#panel')
?.classList.add('ready');
}, []);
動く場合はあります。
ただ、これではReactへ移した意味が薄くなります。
まず、
<button onClick={handleClick}>
にできないか考えます。
readyが画面状態なら、
<div className={ready ? 'panel ready' : 'panel'}>
と表せないか考えます。
Effectは便利ですが、DOM操作の避難場所として使い始めると、jQuery時代とReactの管理方法が一つのコンポーネントに混在します。
Strict ModeでEffectが二度動いたように見える
開発時に「Effectの処理が二回呼ばれている」と戸惑うことがあります。
Reactの公式ドキュメントでは、Strict Modeが有効な開発環境で、Effectについて本番のsetup前に追加のsetupとcleanupを行うことが説明されています。cleanupがsetupを正しく打ち消せるか確認するためのものです。
そのため、
useEffect(() => {
subscribe();
return () => {
unsubscribe();
};
}, []);
のように、開始と終了を対にします。
「二回呼ばれるからフラグで無理に一回にする」より、再実行されても整合性を保てる処理になっているかを見るほうが大切です。
fetch()の404をcatchできると思ってしまう
先ほど触れた通り、HTTPエラーとネットワークエラーは分けて考えます。
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
// 必要なエラー処理
}
既存の$.ajax()から移すときは、成功・失敗の判定条件を確認します。
認証切れ、バリデーションエラー、サーバーエラーなどをアプリケーション側でどう分類しているかも、合わせて確認したいところです。
DOMに保存されている「見えない状態」を見落とす
移行で難しいのはコード量ではなく、DOMが状態管理まで担当しているケースです。
const selected = $('.row.selected');
const id = selected.data('id');
const status = selected.attr('data-status');
このコードでは、
- どの行が選択中か
- その行のID
- 現在のステータス
がDOMにあります。
Reactへ移すなら、先にデータ構造を考えます。
const [selectedId, setSelectedId] = useState(null);
const selectedUser = users.find(
(user) => user.id === selectedId
);
こうすると、表示はデータの結果になります。
既存コードを読むときは、.data()、class、hidden input、style属性などが単なる表示用なのか、アプリケーション状態の保存場所になっているのかを確認しています。
全部Reactにしようとしない
jQueryを見つけたら全部消す、という方針が常に正しいとは考えていません。
古い画面には、長く使われているプラグインや複雑な入力補助、サーバー側テンプレートとの結合が残っていることがあります。
それを一度にReactへ移すと、変更範囲が広がります。
私は古いコードの作り直しでは、「新しい技術をどこまで入れるか」より先に「どこで境界を作れるか」を考えます。
読み取り専用の一覧からReactへ移し、編集フォームは既存のまま残す。あるいは新しく追加する画面からReactにする。そうした分割も選択肢です。
移行の目的はjQueryという文字列をゼロにすることではなく、変更しやすい構造へ近づけることです。
✅ 移行レビュー用チェックリスト
最後に、jQueryコードをReactへ移すときに使える形でチェック項目をまとめます。
そのままPull Requestや移行チケットへ貼れるようにしています。
jQuery → React 移行チェックリスト
[状態]
□ classに保存されている状態がないか
□ data-*に保存されている状態がないか
□ hidden inputが状態管理に使われていないか
□ DOMの有無そのものが状態になっていないか
□ stateとして名前を付けられる値を洗い出したか
[イベント]
□ .on()をJSXのイベントハンドラへ移せるか
□ documentやbodyへのイベント登録が本当に必要か
□ グローバルイベントにはcleanupがあるか
□ イベントデリゲーションをそのまま再現しようとしていないか
[表示]
□ .show() / .hide()を条件付きレンダリングへ移せるか
□ addClass() / removeClass()をstateから決められるか
□ .text()で書き換えている値をJSXで表示できるか
□ .html()で生成しているUIをコンポーネント化できるか
[フォーム]
□ .val()で取得している値を整理したか
□ controlled / uncontrolledのどちらにするか決めたか
□ バリデーション結果をDOMだけに保存していないか
□ submit時の既存挙動を確認したか
[通信]
□ $.ajax()のsuccess相当を確認したか
□ $.ajax()のerror相当を確認したか
□ HTTPエラーの扱いを確認したか
□ loading状態をstateとして持つか決めたか
□ 二重送信を防ぐ必要があるか確認したか
[DOM操作]
□ focusはrefで扱えるか
□ scrollはrefで扱えるか
□ サイズ測定など直接DOMが必要な理由が明確か
□ React管理DOMをjQuery側から変更していないか
□ jQuery管理DOMをReact側から不用意に変更していないか
[外部ライブラリ]
□ 初期化処理をEffectへ置く理由があるか
□ cleanupで破棄できるか
□ ライブラリとReactが同じDOMを管理していないか
□ React対応ライブラリへの交換が本当に必要か検討したか
[段階移行]
□ React化する画面領域を明確にしたか
□ 既存領域とのデータ受け渡し方法を決めたか
□ 一度に書き直す範囲を広げすぎていないか
□ 移行前後で守るべき業務ルールを確認したか
このチェックリストで特に重視しているのは、Reactの文法ではありません。
「状態はいまどこにあるか」です。
そこが分かれば、jQueryのコードが多少複雑でも、React側でどう分解するか考えやすくなります。
まとめ
jQueryからReactへの移行では、「このjQuery APIに対応するReact APIは何か」と考えるだけでは足りません。
jQueryではDOMそのものが、表示であり、状態の保存場所であり、イベントの接続先でもあります。一方、ReactではJavaScript側のstateやpropsを基準にUIを組み立てます。
そのため私は、既存コードを次のように読み替えます。
DOMを書き換えている
↓
何の状態を表している?
↓
stateとして名前を付けられる?
↓
JSXをそのstateから作れる?
↓
無理ならref / Effectが必要か確認する
.show()を見つけたらdisplay: noneの代替を探すのではなく、「何が起きたとき表示されるのか」を探す。
.addClass('selected')を見つけたらclass操作の代替を探すのではなく、「何が選択されているのか」を探す。
.data()を見つけたら、「なぜそのデータがDOMに置かれているのか」を見る。
この読み方に変えると、jQueryからReactへの移行はAPIの翻訳ではなく、画面の状態と責務を整理する作業になります。
古いコードを作り直すときほど、コードを書く前に「この処理は何を表しているのか」を整理することが大切だと考えています。

