「あの時、もっとうまく書けたはず……」
個人開発でReactを使ったアプリを作り、数ヶ月経ってからコードを見返したとき、誰もが一度はこう思うのではないでしょうか? 私も先日、過去に作成したToDo管理アプリのソースコードを整理していたところ、当時は気づかなかった改善点や新たな学びが次々と浮かび上がってきました。
この記事では、Reactアプリのリファクタリングを通じて得られた気づきや、最新のベストプラクティスを紹介します。「コードの腐敗」を防ぐ設計原則から、パフォーマンス最適化の意外な盲点まで、実践的な知見を余すところなくお伝えします。
目次
Toggle1. コンポーネント設計の「成長痛」とその処方箋
当時の私は「とにかく動けばOK」という姿勢で、コンポーネント分割をなおざりにしていました。結果、300行を超えるモンスターコンポーネントが誕生していたのです。
// Before: すべてのロジックが1つのファイルに
const TodoApp = () => {
// 状態管理、API呼び出し、UIレンダリングが渾然一体
const [todos, setTodos] = useState([]);
const fetchData = async () => { /* ... */ };
const handleSubmit = () => { /* ... */ };
const toggleComplete = () => { /* ... */ };
return (
<div>
{/* 膨大なJSX */}
</div>
);
}
このようなコードには明らかな問題があります:
- テストが困難
- 機能追加のたびに複雑度が爆発
- ロジックの再利用が不可能
解決策:Atomic Designによる再構築
Atomic Designの概念を取り入れ、コンポーネントを次のレベルで分割しました:
レベル | 役割 | 例 |
---|---|---|
Atoms | UIの最小要素 | Button, Input |
Molecules | Atomsの組み合わせ | SearchBar |
Organisms | 独立した機能ブロック | TodoList |
Templates | ページレイアウト | MainLayout |
Pages | 実際のビュー | HomePage |
この再設計により、変更に強い構造が生まれました。例えば、Todoアイテムのスタイルを変更する場合、Atoms/TodoItem.jsx
のみを修正すれば良くなったのです。
2. 状態管理の進化論:useStateからZustandへ
初期バージョンでは、ReactのuseState
とuseContext
で状態管理を実装していました。しかし、次の問題に直面しました:
- プロップドリリング:深い階層のコンポーネントにデータを渡すのが面倒
- 不要な再レンダリング:Contextの値が更新されると、すべての子コンポーネントが再描画
Zustandによるモダンな解決
軽量状態管理ライブラリのZustandを導入したところ、劇的な改善が見られました:
// store/todoStore.js
import create from 'zustand';
const useTodoStore = create((set) => ({
todos: [],
addTodo: (text) => set(state => ({
todos: [...state.todos, { text, completed: false }]
})),
toggleTodo: (id) => set(state => ({
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
}))
}));
// コンポーネントでの使用例
const TodoList = () => {
const { todos, toggleTodo } = useTodoStore();
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} {...todo} onToggle={toggleTodo} />
))}
</ul>
);
}
Zustandの利点:
- シンプルなAPI:Reduxのようなボイラープレートコード不要
- 最適化された再レンダリング:必要な状態のみを選択的に購読可能
- ミドルウェアサポート:永続化やデバッグが容易
3. パフォーマンス最適化の意外な落とし穴
「React.memoで囲めばすべて解決」と考えていた時期がありました。しかし、実際には逆効果になるケースも多いことに気づきました。
本当に必要な最適化を見極める
// 非効率な最適化の例
const TodoItem = React.memo(({ todo, onToggle }) => {
return (
<li onClick={() => onToggle(todo.id)}>
{todo.text}
</li>
);
});
// より効果的なアプローチ
const TodoItem = ({ todo, onToggle }) => {
const handleToggle = useCallback(() => {
onToggle(todo.id);
}, [onToggle, todo.id]);
return (
<li onClick={handleToggle}>
{todo.text}
</li>
);
};
パフォーマンス改善の黄金律:
- まずは計測(React DevToolsのProfilerを使用)
- ボトルネックを特定
- 必要最小限の最適化を適用
4. テスト戦略の見直しで得られた安定性
当時は「テストより機能実装」を優先していたため、リグレッション(退行)バグが頻発していました。今回の見直しで、次のテスト戦略を導入しました:
- 単体テスト:Jestでロジック関数をカバー
- コンポーネントテスト:React Testing LibraryでUIの挙動を検証
- E2Eテスト:Cypressでユーザーフロー全体を保証
// テストの実例(React Testing Library)
import { render, screen, fireEvent } from '@testing-library/react';
import TodoApp from './TodoApp';
test('新しいTodoを追加できる', () => {
render(<TodoApp />);
const input = screen.getByPlaceholderText('新しいタスク');
const button = screen.getByText('追加');
fireEvent.change(input, { target: { value: 'テストタスク' } });
fireEvent.click(button);
expect(screen.getByText('テストタスク')).toBeInTheDocument();
});
5. 学びを次に活かすための3つの習慣
このリファクタリング経験から、継続的に成長するために次の習慣を取り入れ始めました:
- 定期的なコードレビュー:1ヶ月に1度、過去のコードを見直す時間を確保
- 技術負債の可視化:TODOコメントをGitHub Issuesに移行し優先順位付け
- 実験用サンドボックス:新しい技術は小さな実験プロジェクトで試す
次なるステップへ
コードを見直す行為は、過去の自分との対話です。当時の判断を批判するのではなく、「あの時の知識で精一杯やった」と認めることが大切だと気づきました。
もしあなたも過去のプロジェクトを見返す機会があれば、ぜひ次の質問を考えてみてください:
- 今の知識でどう改善できるか?
- 当時の制約条件(時間、スキルレベル)を考慮すると、本当に悪いコードだったか?
- この経験を次のプロジェクトにどう活かせるか?
Reactのエコシステムは日々進化しています。かつての「ベストプラクティス」が今ではアンチパターンになっていることも珍しくありません。重要なのは、絶えず学び続ける姿勢です。
あなたのReactアプリを見直すと、どんな発見があるでしょうか? もし興味があれば、私のGitHubリポジトリで実際のコード比較を公開していますので、参考にしてみてください。
(※この記事の内容を実践する際は、必ず最新のReactドキュメントを参照してください:React公式ドキュメント)