こんにちは!KENTEM 第2開発部のフロントエンドエンジニアM・Sです。
KENTEM では、React を用いてフロントエンドを開発しています。初めのうちは React の独特な書き方に抵抗を感じていましたが、今では React なしで開発することなんて考えられません。
そこで今回は、あえて React を使わずに Todoアプリを作ることで React の魅力とありがたみを再認識してみたいと思います!
今回作るもの
以下のような簡単な Todoアプリを作ってみます。
スタイルは tailwind を使って Gemini君が書いてくれました。
それぞれのアイテムは編集と削除が可能になっています。
実装
どちらもあえて1ファイルで実装します。
コードは長めですが、雰囲気だけでも掴んでいただければ幸いです。
純粋なJavaScript
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Todoリスト</title> <script src="https://cdn.tailwindcss.com" rel="stylesheet"></script> </head> <body> <div class="container mx-auto p-4 max-w-md"> <h1 class="text-3xl font-bold mb-4">Todoリスト</h1> <form id="todo-form" class="flex mb-4"> <input type="text" id="todo-input" placeholder="Todoを入力" class="border border-gray-400 p-2 rounded-l-md flex-grow focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-r-md" > 登録 </button> </form> <ul id="todo-list" class="space-y-2"></ul> </div> <script> const todoList = document.getElementById('todo-list'); const todoForm = document.getElementById('todo-form'); const todoInput = document.getElementById('todo-input'); let todos = []; const renderTodoItem = (todo, index) => { const listItem = document.createElement('li'); listItem.classList.add('flex', 'items-center', 'bg-white', 'shadow-inner', 'drop-shadow-xl', 'rounded', 'p-2'); if (todo.editing) { // 編集中の場合はinputを表示 const input = document.createElement('input'); input.type = 'text'; input.value = todo.text; input.classList.add('flex-grow', 'mr-2', 'border', 'border-gray-400', 'p-2', 'rounded', 'focus:outline-none', 'focus:ring-2', 'focus:ring-blue-500'); input.addEventListener('blur', () => { updateTodo(index, input.value); }); input.addEventListener('keydown', (event) => { if (event.key === 'Enter') { updateTodo(index, input.value); } }); listItem.appendChild(input); } else { // 編集中でない場合はspanを表示 const todoSpan = document.createElement('span'); todoSpan.textContent = todo.text; todoSpan.classList.add('flex-1', 'px-2', 'py-1', 'cursor-pointer', 'text-gray-800', 'hover:text-gray-600', 'rounded'); todoSpan.addEventListener('click', () => { editTodo(index); }); listItem.appendChild(todoSpan); } const deleteButton = document.createElement('button'); deleteButton.textContent = '削除'; deleteButton.classList.add('ml-2', 'px-2', 'py-1', 'text-sm', 'font-medium', 'text-red-500', 'hover:text-red-700'); deleteButton.addEventListener('click', () => deleteTodo(index)); listItem.appendChild(deleteButton); return listItem; }; const renderTodos = () => { todoList.innerHTML = ''; todos.forEach((todo, index) => { const listItem = renderTodoItem(todo, index); todoList.appendChild(listItem); }); }; const addTodo = (event) => { event.preventDefault(); const newTodoText = todoInput.value; if (newTodoText !== '') { todos.push({ text: newTodoText, editing: false }); todoInput.value = ''; renderTodos(); } }; const deleteTodo = (index) => { todos.splice(index, 1); renderTodos(); }; const editTodo = (index) => { todos[index].editing = true; renderTodos(); // 入力欄にフォーカスを当てる const input = todoList.children[index].querySelector('input'); input.focus(); }; const updateTodo = (index, newText) => { todos[index].text = newText; todos[index].editing = false; renderTodos(); }; todoForm.addEventListener('submit', addTodo); renderTodos(); </script> </body> </html>
読むのが億劫な方も多いのではないでしょうか...
解説
初期表示
- 変数 todoList, todoForm, todoInput, todos が初期化される
- todoForm が submit された際に addTodo が発火することを指定
- renderTodos が呼び出される(初回は何も描画されない)
登録処理
- addEventListener により addTodo が呼び出される
- input にテキストを入力して submit すると addTodo が呼び出される
- テキストが空白でなければ todos に push して再度 renderTodos を呼び出す
描画処理
- renderTodos は todos の要素数の数だけ renderTodo を呼び出す
- renderTodo は li要素を作る
- 編集中の場合
- li に input要素と value・イベントを追加する
- 編集中でない場合
- li に span要素と todo の text を埋め込む
- クリックされたら編集中になるようにイベントを追加する
- li に span要素と todo の text を埋め込む
- 削除ボタンと削除イベントの定義も renderTodo で行う
- 編集中の場合
削除処理
- index を取得し、deleteTodo で splice を使って削除
編集処理
- 指定された index の todo の editing を true にする
- renderTodos を呼び出し、すべての todo を再描画する
- input にフォーカスを当てる
- 完了したら updateTodo を呼び出し上書きしたのちに renderTodos をもう一度呼び出す
所感
めんどくさい!!!!
後半はAIに手伝ってもらった部分もあるとはいえ、JavaScript単体だと辛いものがありました。手続型のプログラミングではどうしてもコードが複雑化しやすいですね。特にDOM操作が絡むと、要素の取得、イベントリスナーの設定、状態の更新など、処理が散らばってしまいました。結果として、可読性や保守性が損なわれているコードになってしまいました。
続いてお待ちかねReactです。
React
import React, { useState } from "react"; import { v4 as uuid } from "uuid"; const App = () => { const [input, setInput] = useState(""); const [todos, setTodos] = useState([]); const handleDelete = (index) => { setTodos((prevTodos) => prevTodos.filter((_, i) => i !== index)); }; const handleSubmit = (e) => { e.preventDefault(); if (input.trim() === "") return; setTodos((t) => [...t, input]); setInput(""); }; return ( <div className="container mx-auto p-4 max-w-md"> <h1 className="text-3xl font-bold mb-4">Todoリスト</h1> <form onSubmit={handleSubmit} className="flex mb-4"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder="Todoを入力" className="border border-gray-400 p-2 rounded-l-md flex-grow focus:outline-none focus:ring-2 focus:ring-blue-500" /> <button type="submit" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-r-md" > 登録 </button> </form> <ul className="space-y-2"> {todos.map((todo, i) => ( <li className="flex items-center bg-white shadow-inner drop-shadow-xl rounded p-2"> <Item key={uuid()} todo={todo} onEdit={(updatedTodo) => { setTodos((prevTodos) => prevTodos.map((t, j) => (j === i ? updatedTodo : t)) ); }} /> <button onClick={() => handleDelete(i)} className="ml-2 px-2 py-1 text-sm font-medium text-red-500 hover:text-red-700" > 削除 </button> </li> ))} </ul> </div> ); }; const Item = ({ todo, onEdit }) => { const [edit, setEdit] = useState(false); const [text, setText] = useState(todo); const handleKeyDown = (e) => { if (e.key === "Enter") { setEdit(false); onEdit(text); } }; return ( <> {edit ? ( <input type="text" value={text} onChange={(e) => setText(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => { setEdit(false); onEdit(text); }} className="flex-grow mr-2 border border-gray-400 p-2 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" /> ) : ( <span onClick={() => setEdit(true)} className="flex-1 px-2 py-1 cursor-pointer text-gray-800 hover:text-gray-600 rounded" > {text} </span> )} </> ); }; export default App;
解説
初期表示
- todos と input の useState を初期化
- App コンポーネントと Item コンポーネントが読み込まれる
登録処理
- フォームに入力されたテキストを 状態input に保持。
- handleSubmit 関数が実行され、todos に値が追加される
- useState が更新されることでレンダリングが行われる
描画処理
- useState が更新されるたびにレンダリングが行われる
削除処理
- handleDelete 関数が実行され、todos 状態から該当Todoを削除
- useStateが更新されるのでレンダリングは自動で行われる
編集処理
- Item コンポーネントの状態editがtrueに切り替わる
- Item コンポーネントがspanからinputに切り替わる
- フォーカスが外れるとeditがfalseに切り替わりspanに戻る
- useStateが更新されるのでレンダリングは自動で行われる
所感
読みやすい!!!!!
useState を使うことで、DOM操作を意識することなく状態管理ができ、更新も容易に行えています。useStateが更新された際に再レンダリングされるので、描画について意識する必要はほぼありません(仮想DOM についてはまたどこかで)。また、Item をコンポーネントとして切り分けることで、ロジックと JSX の役割が明確になり、可読性や保守性を高めることができています。
まとめ
今回は、あえて React を使わずにシンプルな Todoアプリを作成することで、React を使うメリットを改めて実感することができました。
純粋な JavaScript で実装した Todoアプリは、DOM 操作やイベント処理が複雑になり、コードの可読性や保守性の面で課題が残りました。
一方、React を使った実装では、以下のような点が優れていました。
- 宣言的なUI記述による状態管理の容易さ:React では、useState などを用いることで、状態と UI の同期を宣言的に記述できます。そのため、DOM操作を直接行う必要がなくなり、状態の変化が UI にどのように反映されるかを容易に管理できます。
- コンポーネントベース開発:Itemコンポーネントのように、UI を独立したコンポーネントに分割することで、コードの再利用性や保守性を高めることができました。また、コンポーネントごとに状態やロジックをカプセル化することで、コード全体の複雑さを軽減できました。
これらの特徴により、React を使うことでより可読性・保守性・拡張性が高いアプリケーション開発が可能になります。
今回の執筆を通して、React の魅力とありがたみを再認識することができました! これからもReact を使い続けながら、理解を深め、さらに快適で効率的な開発を目指していきたいと思います!
おわりに
KENTEMでは、様々な拠点でエンジニアを大募集しています!
建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。
recruit.kentem.jp
career.kentem.jp