React初心者が陥るアンチパターン10選

React初心者がよく陥るアンチパターンを10個紹介。間違った書き方と正しい書き方を比較しながら、ベストプラクティスを解説します。

Learning Next 運営
31 分で読めます

みなさん、React初心者の頃を思い出してみてください。

「なんでこのコードが動かないんだろう?」 「書き方が間違っているのかな?」

こんな風に悩んだことはありませんか? 実は、React初心者の方が陥りやすい**「アンチパターン」**があるんです。

アンチパターンとは、一見正しく見えるけど、実は問題を引き起こす可能性があるコードパターンのことです。

この記事では、React初心者がよく陥るアンチパターンを10個紹介します。 間違った書き方と正しい書き方を比較しながら、わかりやすく解説していきますね。

これらのパターンを知ることで、より良いReactアプリケーションを作れるようになりますよ!

アンチパターンって何?

アンチパターンについて、まずは基本的な部分から説明しますね。

簡単に言うと、アンチパターンとは**「一見問題なさそうに見えるけど、実は問題を引き起こす可能性のあるコードパターン」**のことです。

なぜアンチパターンを学ぶの?

アンチパターンを学ぶことで、こんなメリットがあります。

学習効果について

  • 間違いを事前に防げるようになる
  • コードの品質が向上する
  • デバッグの時間が短縮される

実践的な理由

  • チーム開発での統一性が保てる
  • 保守性が向上する
  • パフォーマンスが最適化される

難しそうに見えるかもしれませんが、大丈夫です! 一つずつ丁寧に説明していきますね。

1. stateを直接変更してしまう

最も多い間違いが、stateを直接変更してしまうことです。

React初心者の方がよく陥るパターンなので、まずはこちらから見ていきましょう。

❌ 間違った書き方

function TodoList() {
    const [todos, setTodos] = useState([
        { id: 1, text: '買い物', completed: false }
    ]);
    
    const toggleTodo = (id) => {
        // 間違い:stateを直接変更
        const todo = todos.find(t => t.id === id);
        todo.completed = !todo.completed;
        setTodos(todos); // 再レンダリングが発生しない
    };
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
                    {todo.text} - {todo.completed ? '完了' : '未完了'}
                </li>
            ))}
        </ul>
    );
}

この書き方では、元のtodoオブジェクトを直接変更しています。 findメソッドで見つけたtodoオブジェクトのcompletedプロパティを直接変更してしまっているんです。

これだとReactは変更を検知できないので、再レンダリングが発生しません。

✅ 正しい書き方

function TodoList() {
    const [todos, setTodos] = useState([
        { id: 1, text: '買い物', completed: false }
    ]);
    
    const toggleTodo = (id) => {
        // 正しい:新しいオブジェクトを作成
        setTodos(todos.map(todo => 
            todo.id === id 
                ? { ...todo, completed: !todo.completed }
                : todo
        ));
    };
    
    return (
        <ul>
            {todos.map(todo => (
                <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
                    {todo.text} - {todo.completed ? '完了' : '未完了'}
                </li>
            ))}
        </ul>
    );
}

正しい書き方では、mapメソッドを使って新しい配列を作成しています。 該当するtodoには...todoでスプレッド演算子を使って新しいオブジェクトを作成し、completedプロパティだけを変更しています。

他のtodoはそのまま返しているので、無駄な処理もありません。

なぜこの書き方が問題なの?

  • Reactは参照が同じオブジェクトの変更を検知できない
  • 再レンダリングが発生しない
  • 予期しない動作を引き起こす

2. 配列のindexをkeyに使用する

リストをレンダリングする時に、indexをkeyに使うのは危険です。

❌ 間違った書き方

function UserList() {
    const [users, setUsers] = useState([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' }
    ]);
    
    const addUser = () => {
        const newUser = { id: Date.now(), name: '新規ユーザー' };
        setUsers([newUser, ...users]); // 先頭に追加
    };
    
    return (
        <div>
            <button onClick={addUser}>ユーザー追加</button>
            {users.map((user, index) => (
                // 間違い:indexをkeyに使用
                <div key={index}>
                    <input defaultValue={user.name} />
                </div>
            ))}
        </div>
    );
}

この書き方では、mapメソッドの第二引数であるindexをkeyに使っています。

問題は、新しいユーザーを先頭に追加した時に起こります。 各要素のindexが変わってしまうので、Reactは要素を正しく識別できません。

✅ 正しい書き方

function UserList() {
    const [users, setUsers] = useState([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' }
    ]);
    
    const addUser = () => {
        const newUser = { id: Date.now(), name: '新規ユーザー' };
        setUsers([newUser, ...users]);
    };
    
    return (
        <div>
            <button onClick={addUser}>ユーザー追加</button>
            {users.map(user => (
                // 正しい:ユニークなIDをkeyに使用
                <div key={user.id}>
                    <input defaultValue={user.name} />
                </div>
            ))}
        </div>
    );
}

正しい書き方では、user.idをkeyに使用しています。 user.idは各ユーザーに固有のIDなので、要素の並び順が変わってもReactは正しく要素を識別できます。

なぜこの書き方が問題なの?

  • 要素の並び順が変わると、間違った要素が更新される
  • パフォーマンスが低下する
  • 予期しない動作を引き起こす

3. useEffectの依存配列を省略する

useEffectで依存配列を省略するのは、とても危険です。

❌ 間違った書き方

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    
    useEffect(() => {
        // 間違い:依存配列なし
        fetchUser(userId).then(setUser);
    }); // 依存配列が省略されている
    
    return <div>{user?.name}</div>;
}

この書き方では、useEffectの第二引数(依存配列)を省略しています。

これだと、コンポーネントが再レンダリングされるたびにuseEffectが実行されてしまいます。 つまり、無限ループが発生する可能性があります。

✅ 正しい書き方

function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    
    useEffect(() => {
        // 正しい:依存配列を指定
        fetchUser(userId).then(setUser);
    }, [userId]); // userIdが変わった時のみ実行
    
    return <div>{user?.name}</div>;
}

正しい書き方では、[userId]を依存配列として指定しています。

これにより、userIdが変わった時のみuseEffectが実行されます。 同じuserIdでコンポーネントが再レンダリングされても、useEffectは実行されません。

なぜこの書き方が問題なの?

  • 無限ループが発生する可能性がある
  • 不要な再実行でパフォーマンスが低下する
  • 予期しない副作用が発生する

4. 無駄な再レンダリングを引き起こす

不必要な再レンダリングを引き起こすコードは、パフォーマンスを悪化させます。

❌ 間違った書き方

function Parent() {
    const [count, setCount] = useState(0);
    
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                カウント: {count}
            </button>
            {/* 間違い:毎回新しいオブジェクトを作成 */}
            <ExpensiveChild style={{ color: 'red' }} />
        </div>
    );
}

function ExpensiveChild({ style }) {
    console.log('ExpensiveChild がレンダリングされました');
    
    // 重い処理
    const result = Array(1000000).fill(0).reduce((sum, _, i) => sum + i, 0);
    
    return <div style={style}>結果: {result}</div>;
}

この書き方では、<ExpensiveChild>に渡しているstyleプロパティが毎回新しいオブジェクトとして作成されています。

{ color: 'red' }は見た目は同じでも、毎回新しいオブジェクトなので、ExpensiveChildは常に再レンダリングされてしまいます。

✅ 正しい書き方

function Parent() {
    const [count, setCount] = useState(0);
    
    // 正しい:オブジェクトを外部で定義
    const childStyle = { color: 'red' };
    
    return (
        <div>
            <button onClick={() => setCount(count + 1)}>
                カウント: {count}
            </button>
            <ExpensiveChild style={childStyle} />
        </div>
    );
}

// React.memoで最適化
const ExpensiveChild = React.memo(({ style }) => {
    console.log('ExpensiveChild がレンダリングされました');
    
    const result = Array(1000000).fill(0).reduce((sum, _, i) => sum + i, 0);
    
    return <div style={style}>結果: {result}</div>;
});

正しい書き方では、childStyleをコンポーネント外で定義しています。 また、React.memoを使ってExpensiveChildをメモ化しています。

これにより、styleプロパティが変わらない限り、ExpensiveChildは再レンダリングされません。

なぜこの書き方が問題なの?

  • 無駄な再計算が発生する
  • パフォーマンスが低下する
  • UIの応答性が悪くなる

5. propsを過度に分割する

propsを過度に分割するのも、問題を引き起こします。

❌ 間違った書き方

function UserCard({ 
    userId, 
    userName, 
    userEmail, 
    userPhone, 
    userAddress, 
    userAge, 
    userRole 
}) {
    return (
        <div>
            <h3>{userName}</h3>
            <p>メール: {userEmail}</p>
            <p>電話: {userPhone}</p>
            <p>住所: {userAddress}</p>
            <p>年齢: {userAge}</p>
            <p>役職: {userRole}</p>
        </div>
    );
}

// 使用時
<UserCard 
    userId={user.id}
    userName={user.name}
    userEmail={user.email}
    userPhone={user.phone}
    userAddress={user.address}
    userAge={user.age}
    userRole={user.role}
/>

この書き方では、ユーザー情報の各プロパティを個別のpropsとして渡しています。

確かに、各プロパティが明確になりますが、propsの数が多すぎて扱いにくくなっています。

✅ 正しい書き方

function UserCard({ user }) {
    return (
        <div>
            <h3>{user.name}</h3>
            <p>メール: {user.email}</p>
            <p>電話: {user.phone}</p>
            <p>住所: {user.address}</p>
            <p>年齢: {user.age}</p>
            <p>役職: {user.role}</p>
        </div>
    );
}

// 使用時
<UserCard user={user} />

正しい書き方では、userオブジェクトをそのまま渡しています。

これにより、propsの数が大幅に削減され、コードがシンプルになります。 また、新しいユーザー情報が追加されても、コンポーネントの呼び出し部分を変更する必要がありません。

なぜこの書き方が問題なの?

  • コードが冗長になる
  • 保守性が低下する
  • 型定義が複雑になる

6. 条件付きレンダリングの間違い

条件付きレンダリングで間違った書き方をしてしまうパターンです。

❌ 間違った書き方

function UserList({ users }) {
    return (
        <div>
            {/* 間違い:数値0がレンダリングされる */}
            {users.length && (
                <div>
                    {users.map(user => (
                        <div key={user.id}>{user.name}</div>
                    ))}
                </div>
            )}
        </div>
    );
}

この書き方では、users.lengthが0の時に問題が発生します。

JavaScriptでは、0 && 何かの結果は0になります。 そのため、ユーザーがいない時に画面に「0」が表示されてしまいます。

✅ 正しい書き方

function UserList({ users }) {
    return (
        <div>
            {/* 正しい:boolean値で判定 */}
            {users.length > 0 && (
                <div>
                    {users.map(user => (
                        <div key={user.id}>{user.name}</div>
                    ))}
                </div>
            )}
            
            {/* または三項演算子を使用 */}
            {users.length > 0 ? (
                <div>
                    {users.map(user => (
                        <div key={user.id}>{user.name}</div>
                    ))}
                </div>
            ) : (
                <div>ユーザーがいません</div>
            )}
        </div>
    );
}

正しい書き方では、users.length > 0で明示的にboolean値を作成しています。

これにより、ユーザーがいない時は何も表示されません。 または、三項演算子を使って「ユーザーがいません」という説明文を表示することもできます。

なぜこの書き方が問題なの?

  • 数値0がそのまま表示される
  • 予期しない表示結果になる
  • UIの見た目が崩れる

7. イベントハンドラーで関数を即座に実行する

イベントハンドラーで関数を即座に実行してしまうパターンです。

❌ 間違った書き方

function TodoItem({ todo, onToggle, onDelete }) {
    return (
        <div>
            <span>{todo.text}</span>
            {/* 間違い:関数を即座に実行 */}
            <button onClick={onToggle(todo.id)}>
                切り替え
            </button>
            <button onClick={onDelete(todo.id)}>
                削除
            </button>
        </div>
    );
}

この書き方では、onToggle(todo.id)onDelete(todo.id)を即座に実行しています。

これだと、レンダリング時に関数が実行されてしまいます。 ボタンをクリックした時ではなく、コンポーネントが表示された時に実行されるんです。

✅ 正しい書き方

function TodoItem({ todo, onToggle, onDelete }) {
    return (
        <div>
            <span>{todo.text}</span>
            {/* 正しい:関数を返す */}
            <button onClick={() => onToggle(todo.id)}>
                切り替え
            </button>
            <button onClick={() => onDelete(todo.id)}>
                削除
            </button>
        </div>
    );
}

正しい書き方では、アロー関数を使って() => onToggle(todo.id)のように書いています。

これにより、ボタンがクリックされた時に関数が実行されます。 アロー関数は「関数を返す関数」なので、イベントハンドラーとして正しく動作します。

なぜこの書き方が問題なの?

  • レンダリング時に関数が実行される
  • 予期しない動作が発生する
  • パフォーマンスが低下する

8. useStateの初期値で関数を直接呼び出す

useStateの初期値で関数を直接呼び出してしまうパターンです。

❌ 間違った書き方

function ExpensiveComponent() {
    // 間違い:毎回レンダリング時に関数が実行される
    const [data, setData] = useState(expensiveCalculation());
    
    return <div>{data}</div>;
}

function expensiveCalculation() {
    console.log('重い計算が実行されました');
    return Array(1000000).fill(0).reduce((sum, _, i) => sum + i, 0);
}

この書き方では、expensiveCalculation()を直接呼び出しています。

これだと、コンポーネントが再レンダリングされるたびに重い計算が実行されてしまいます。 本来は初回のみ実行されれば十分なのに、毎回実行されるのは無駄ですよね。

✅ 正しい書き方

function ExpensiveComponent() {
    // 正しい:関数を渡すことで初回のみ実行
    const [data, setData] = useState(() => expensiveCalculation());
    
    return <div>{data}</div>;
}

function expensiveCalculation() {
    console.log('重い計算が実行されました');
    return Array(1000000).fill(0).reduce((sum, _, i) => sum + i, 0);
}

正しい書き方では、() => expensiveCalculation()のように関数を渡しています。

これにより、初回のレンダリング時のみ重い計算が実行されます。 その後の再レンダリングでは、既に計算された値がそのまま使われるので、パフォーマンスが大幅に向上します。

なぜこの書き方が問題なの?

  • 毎回レンダリング時に重い処理が実行される
  • 初期化のためだけの処理が無駄に実行される
  • パフォーマンスが著しく低下する

9. 同期的な処理で不適切にuseEffectを使用する

useEffectを不適切に使用するパターンです。

❌ 間違った書き方

function SearchComponent() {
    const [searchTerm, setSearchTerm] = useState('');
    const [results, setResults] = useState([]);
    
    // 間違い:同期的な処理でuseEffectを使用
    useEffect(() => {
        setResults(
            searchTerm 
                ? mockData.filter(item => 
                    item.name.includes(searchTerm)
                  )
                : []
        );
    }, [searchTerm]);
    
    return (
        <div>
            <input 
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
            />
            {results.map(item => (
                <div key={item.id}>{item.name}</div>
            ))}
        </div>
    );
}

この書き方では、検索処理でuseEffectを使用しています。

しかし、検索処理は同期的な処理なので、useEffectを使う必要がありません。 useEffectは副作用(API呼び出しなど)を扱うためのものです。

✅ 正しい書き方

function SearchComponent() {
    const [searchTerm, setSearchTerm] = useState('');
    
    // 正しい:計算結果を直接使用
    const results = searchTerm 
        ? mockData.filter(item => 
            item.name.includes(searchTerm)
          )
        : [];
    
    return (
        <div>
            <input 
                value={searchTerm}
                onChange={(e) => setSearchTerm(e.target.value)}
            />
            {results.map(item => (
                <div key={item.id}>{item.name}</div>
            ))}
        </div>
    );
}

正しい書き方では、resultsを直接計算しています。

searchTermが変わると、コンポーネントが再レンダリングされて、resultsが自動的に再計算されます。 不要なstateとuseEffectを削除できて、コードがシンプルになります。

なぜこの書き方が問題なの?

  • 不要なstateとuseEffectが増える
  • コードが複雑になる
  • 同期的な処理には副作用は不要

10. コンポーネント内で不適切に関数を定義する

コンポーネント内で関数を不適切に定義するパターンです。

❌ 間違った書き方

function UserProfile({ user }) {
    // 間違い:毎回レンダリング時に関数が再定義される
    const formatDate = (date) => {
        return new Date(date).toLocaleDateString();
    };
    
    const calculateAge = (birthDate) => {
        const today = new Date();
        const birth = new Date(birthDate);
        return today.getFullYear() - birth.getFullYear();
    };
    
    return (
        <div>
            <h3>{user.name}</h3>
            <p>生年月日: {formatDate(user.birthDate)}</p>
            <p>年齢: {calculateAge(user.birthDate)}歳</p>
        </div>
    );
}

この書き方では、formatDatecalculateAgeをコンポーネント内で定義しています。

これだと、コンポーネントが再レンダリングされるたびに関数が再定義されてしまいます。 関数の内容は変わっていないのに、毎回新しい関数が作成されるのは無駄です。

✅ 正しい書き方

// 正しい:コンポーネント外で関数を定義
const formatDate = (date) => {
    return new Date(date).toLocaleDateString();
};

const calculateAge = (birthDate) => {
    const today = new Date();
    const birth = new Date(birthDate);
    return today.getFullYear() - birth.getFullYear();
};

function UserProfile({ user }) {
    return (
        <div>
            <h3>{user.name}</h3>
            <p>生年月日: {formatDate(user.birthDate)}</p>
            <p>年齢: {calculateAge(user.birthDate)}歳</p>
        </div>
    );
}

正しい書き方では、関数をコンポーネント外で定義しています。

これにより、関数は一度だけ定義され、再レンダリングで無駄な再定義が発生しません。 メモリ効率も良くなります。

もし、どうしてもコンポーネント内で定義する必要がある場合は、useCallbackを使用することもできます。

function UserProfileWithCallback({ user }) {
    const formatDate = useCallback((date) => {
        return new Date(date).toLocaleDateString();
    }, []);
    
    const calculateAge = useCallback((birthDate) => {
        const today = new Date();
        const birth = new Date(birthDate);
        return today.getFullYear() - birth.getFullYear();
    }, []);
    
    return (
        <div>
            <h3>{user.name}</h3>
            <p>生年月日: {formatDate(user.birthDate)}</p>
            <p>年齢: {calculateAge(user.birthDate)}歳</p>
        </div>
    );
}

useCallbackを使うことで、関数の再定義を防ぐことができます。

なぜこの書き方が問題なの?

  • 毎回レンダリング時に関数が再作成される
  • メモリの無駄遣いが発生する
  • 子コンポーネントの最適化が効かない

アンチパターンを避けるためのチェックリスト

これらのアンチパターンを避けるために、チェックリストを作成しました。

開発中に活用してくださいね!

開発時のチェックポイント

state管理について

  • stateを直接変更していないか?
  • useStateの初期値で関数を直接呼び出していないか?
  • 不要なstateを作成していないか?

レンダリングについて

  • 配列のindexをkeyに使用していないか?
  • 無駄な再レンダリングを発生させていないか?
  • 条件付きレンダリングで適切な判定を行っているか?

useEffectについて

  • 依存配列を適切に指定しているか?
  • 不要なuseEffectを使用していないか?
  • 副作用のクリーンアップを行っているか?

イベントハンドラーについて

  • 関数を即座に実行していないか?
  • 適切なイベントオブジェクトを使用しているか?

パフォーマンスについて

  • 不要な関数の再定義を避けているか?
  • 適切なメモ化を行っているか?
  • propsの過度な分割を避けているか?

コードレビュー時のチェックポイント

可読性について

  • コンポーネントの責務が明確か?
  • 適切な命名が行われているか?
  • 複雑な処理が適切に分割されているか?

保守性について

  • 再利用可能な設計になっているか?
  • 適切なpropsの設計が行われているか?
  • エラーハンドリングが適切に実装されているか?

これらのチェックポイントを意識することで、より良いReactコードが書けるようになります。

より良いReactコードを書くために

React初心者がよく陥るアンチパターン10選を紹介しました。

たくさんの内容でしたが、大丈夫です! 一度にすべてを覚える必要はありません。

重要なポイントをまとめると

  1. stateの直接変更を避ける - 不変性を保つ
  2. 適切なkeyを使用する - ユニークなIDを使用
  3. useEffectの依存配列を指定する - 不要な実行を防ぐ
  4. 無駄な再レンダリングを避ける - パフォーマンスを最適化
  5. propsの適切な設計 - 保守性を向上させる

学習のアドバイス

  • 一度にすべてを覚えようとしないでください
  • 実際のコードで少しずつ実践してみてください
  • コードレビューで他の人からフィードバックを受けてください
  • 公式ドキュメントやベストプラクティスを参考にしてください

継続的な改善のために

  • 定期的に自分のコードを見直してください
  • 新しいReactの機能やパターンを学んでください
  • チーム内でコーディング規約を設けてください

これらのアンチパターンを理解して避けることで、より保守性が高く、パフォーマンスの良いReactアプリケーションを作成できます。

最初は難しく感じるかもしれませんが、実践を通じて必ず身につきます。 大丈夫です!

ぜひ今日から意識して、より良いReactコードを書いていきましょう! 一歩ずつ確実に上達していけますよ。

関連記事