🍣

useStateとuseReducerの使い分け

2024/03/19に公開

Reactステート管理の最適なアプローチ

Reactには、コンポーネントの状態を管理するための二つのフック、useStateとuseReducerがあります。

これらはどちらもステート管理に使用されますが、用途や扱えるステートの複雑さによって使い分けることが推奨されます。

本記事では、useStateとuseReducerの基本的な使い方から、それぞれが適しているシナリオ、そしてImmerライブラリを利用した複雑なステート管理の簡素化方法について、TypeScriptを用いたコードスニペットと共に解説します。

useStateとuseReducerの基本

Reactにおけるステート管理は、コンポーネントの動的なデータを扱う上で中心的な役割を果たします。useStateuseReducerは、このステート管理を実現するための二つの主要なフックです。ここでは、これらのフックの使用方法と、それぞれがどのような場合に適しているかについて解説します。

useStateの使用例

useStateは、単一のステート値を管理するために使用されます。以下は、カウンターコンポーネントのTypeScriptによる実装例です:

import React, { useState } from 'react';

const CounterComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default CounterComponent;

この例では、useStateフックを使用してcountステートを管理しています。setCount関数によってステートが更新されると、コンポーネントは再レンダリングされます。

useReducerの使用例

useReducerは、より複雑なステートロジックや複数のサブ値を含むステートを管理する際に適しています。以下は、useReducerを使用したカウンターコンポーネントの実装例です:

import React, { useReducer } from 'react';

interface State {
  count: number;
}

type Action = { type: 'increment' } | { type: 'decrement' };

const initialState: State = { count: 0 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

const CounterComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
};

export default CounterComponent;

この例では、アクションタイプに基づいてステートを更新するロジックがreducer関数によって定義されています。useReducerは、複雑なステート管理や、ステートの更新が多くのアクションによって行われる場合に特に有効です。

二つのフックの比較

  • useStateは、単純なステートや少数のステート値を管理する場合に適しています。
  • useReducerは、ステート更新ロジックが複雑である場合や、複数のサブ値を含むステートを扱う場合に有効です。

複雑なステート管理におけるuseReducerの利点

useReducerフックは、Reactにおけるステート管理のための強力なツールです。

特に、複雑なステートロジックや多数の状態を持つ大規模なアプリケーションにおいて、その真価を発揮します。

この章では、useReducerを使用することの主な利点について掘り下げていきます。

状態更新ロジックの分離と再利用性

useReducerを使用する最大の利点の一つは、コンポーネントから状態更新ロジックを分離できることです。これにより、ロジックを再利用しやすくなり、テストが容易になります。

以下は、複数の状態を持つコンポーネントの例です:

import React, { useReducer } from 'react';

interface State {
  count: number;
  text: string;
}

type Action = 
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setText'; payload: string };

const initialState: State = { count: 0, text: '' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'setText':
      return { ...state, text: action.payload };
    default:
      return state;
  }
}

const ComplexComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
      />
    </div>
  );
};

export default ComplexComponent;

この例では、カウントとテキストの両方を管理する複雑なステートをuseReducerで扱っています。

状態更新ロジックがreducer関数に集約されているため、コンポーネント自体はよりシンプルで読みやすくなります。

イベントソースドモデルとしての利用

useReducerは、アプリケーション内で発生するイベントをモデル化するのにも適しています。

各アクションはアプリケーションで発生するイベントを表し、これらのイベントを通じてアプリケーションの状態が更新されます。

これは、特にアプリケーションの状態変更を追跡したい場合や、時間を遡ってデバッグしたい場合に有効です。

複雑なステート変更の明確化

useReducerを使用することで、ステートの変更がどのように行われるかが明確になります。

アクションとその処理方法が一箇所に定義されるため、どのアクションがどのような影響を与えるのかを理解しやすくなります。

以上の利点から、useReducerは複雑なステート管理を必要とするアプリケーションにおいて、useStateよりも適した選択肢となり得ます。

特に、アプリケーションのスケールが大きくなるにつれて、その利点はより顕著になります。

Immerを利用したステート更新の簡素化

複雑なステートオブジェクトの管理は、特にネストされたオブジェクトや配列を含む場合、煩雑になりがちです。

ReactのuseStateuseReducerを使用する際に、Immerライブラリを組み合わせることで、このような複雑なステートの更新を簡単かつ直感的に行うことができます。

Immerは、変更不可能なデータを扱う際の課題を解決するために設計されており、ミュータブルな操作を行いながらも、背後では不変性を保持した新しいステートを生成します。

useReducerとImmerの統合

以下は、useReducerとImmerを統合した複雑なステート管理のTypeScriptによる例です:

import React, { useReducer } from 'react';
import produce from 'immer';

interface State {
  count: number;
  text: string;
}

type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setText'; payload: string };

const initialState: State = {
  count: 0,
  text: '',
};

const reducer = produce((draft: State, action: Action) => {
  switch (action.type) {
    case 'increment':
      draft.count += 1;
      break;
    case 'decrement':
      draft.count -= 1;
      break;
    case 'setText':
      draft.text = action.payload;
      break;
  }
});

const ComplexComponent: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => dispatch({ type: 'setText', payload: e.target.value })}
      />
    </div>
  );
};

export default ComplexComponent;

この例では、Immerのproduce関数を利用してreducerを作成しています。Immerを使用することで、ステートオブジェクトをミューテートするかのようにコードを書くことができ、それにより読みやすさと記述の簡便さが向上します。

実際には、Immerが背後で不変性を保持した新しいステートオブジェクトを生成しています。

useStateでのImmerの利用

ImmerはuseReducerだけでなく、useStateを使用する場合にも役立ちます。特に、ステートが複雑なオブジェクトやネストされたデータ構造を持つ場合に便利です。以下は、useStateとImmerを組み合わせた例です:

import React, { useState } from 'react';
import produce from 'immer';

interface State {
  count: number;
  text: string;
}

const ComplexComponent: React.FC = () => {
  const [state, setState] = useState<State>({ count: 0, text: '' });

  const increment = () => {
    setState(produce(draft => {
      draft.count += 1;
    }));
  };

  const setText = (text: string) => {
    setState(produce(draft => {
      draft.text = text;
    }));
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
      <p>Text: {state.text}</p>
      <input
        value={state.text}
        onChange={(e) => setText(e.target.value)}
      />
    </div>
  );
};

export default ComplexComponent;

このコードでは、setStateを呼び出す際にproduce関数を使用しています。この方法により、ステートの更新ロジックがより簡潔に、かつ直感的に記述できます。

Immerを使用することで、複雑なステート管理を簡素化し、コードの保守性と可読性を向上させることが可能になります。Reactアプリケーションでのステート管理において、このようなモダンなアプローチを取り入れることで、開発の効率性とアプリケーションのパフォーマンスの両方を向上させることができます。

ベストプラクティスとパフォーマンス

Reactにおけるステート管理はアプリケーションのパフォーマンスに大きな影響を及ぼします。

useStateuseReducerを適切に使い分けることは、効率的で保守しやすいコードを書く上で非常に重要です。

ここでは、これらのフックを使用する際のベストプラクティスと、パフォーマンスへの影響について解説します。

useStateとuseReducerの適切な使い分け

  • 単純なステート値の管理: useStateは値が単一で、その更新ロジックがシンプルな場合に適しています。例えば、フォームの入力値や、トグルスイッチの状態などがこれに該当します。

  • 複雑なステートロジックの管理: useReducerは複数のサブ値を持つステートや、複数のアクションによって状態が更新されるような複雑なロジックを扱う場合に最適です。アクションに基づいてステートを更新する方法を定義することで、より明確で再利用可能なコードを書くことができます。

パフォーマンスへの影響

Reactのステート更新は非同期に行われるため、不要なレンダリングを避けることがパフォーマンス向上に直結します。以下に、パフォーマンスを意識したステート管理のためのヒントをいくつか示します:

  • ステートの分割: 大きなオブジェクト一つをステートとして管理するのではなく、必要に応じて複数のuseStateuseReducerを使用してステートを分割します。これにより、関係のないUI部分の不要な再レンダリングを防ぐことができます。

  • メモ化されたコールバックの使用: useCallbackフックを使用して、イベントハンドラや副作用内で使用する関数をメモ化することで、不要な再計算を避けることができます。

  • 選択的レンダリングの最適化: React.memoを使用して、プロップスが変更されたときにのみコンポーネントが再レンダリングされるようにします。これは、特にリストやアイテムをレンダリングする際に有効です。

ベストプラクティスの採用例

import React, { useState, useCallback } from 'react';

const CounterComponent: React.FC = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

この例では、useCallbackを使用してincrement関数をメモ化しています。

これにより、incrementが依存する値が変更されない限り、同じ関数インスタンスが再利用され、コンポーネントの不要な再レンダリングが防げます。

ステート管理のアプローチを適切に選択し、パフォーマンスへの影響を意識したコーディングを心がけることで、Reactアプリケーションの効率性とユーザーエクスペリエンスを大きく向上させることが可能です。

まとめ

ReactのuseStateuseReducerフックを用いたステート管理の基本から、それぞれの使用シナリオ、Immerを利用したステート更新の簡素化方法、そしてステート管理におけるベストプラクティスとパフォーマンス向上のヒントについて解説しました。

これらの知識を活用することで、Reactアプリケーションのステート管理をより効率的で、保守しやすく、パフォーマンスに優れたものにすることができます。

ステート管理の選択肢

  • useState: 単純なステートや独立したステート値の管理に適しています。
  • useReducer: 複雑なステートロジックや複数のサブステートを含む場合に最適です。
  • Immerの利用: ステート更新時の不変性を保ちつつ、直感的なミュータブルなコード記述を可能にします。

パフォーマンスとベストプラクティス

  • ステートの適切な分割: ステートを適切に分割し、関連のないUIの再レンダリングを防ぎます。
  • メモ化されたコールバックの使用: useCallbackReact.memoを適切に使用して、不要な再計算や再レンダリングを避けます。
  • 選択的レンダリング: コンポーネントのレンダリングは必要な時のみに限定し、パフォーマンスを最適化します。

結論

Reactにおけるステート管理はアプリケーションの基盤となります。useStateuseReducerを使い分けることで、アプリケーションのスケールに応じた柔軟かつ効果的なステート管理が可能になります。

また、Immerを活用することで、ステート更新の複雑さを解消し、開発の生産性を高めることができます。

Reactアプリケーションの開発においては、これらのツールを適切に活用し、常にパフォーマンスを意識することが重要です。

適切なステート管理戦略を採用することで、より応答性が高く、ユーザーにとって快適なアプリケーションを構築しましょう。

この記事が、Reactを用いたアプリケーション開発における効果的なステート管理の理解に役立つことを願っています。

ありがとうございました。

Discussion