ほんとかどうかわかんない話(React②)



ReactはMVVMではない。

データフロー

Reactにおけるデータフローは基本的に「一方向(単方向)」です。このアプローチは、アプリケーションの状態管理をより予測可能かつ理解しやすくするために設計されています。

Reactの一方向データフロー

  1. 親から子へのプロップス: コンポーネントは、そのプロップス(properties)を通じて、親コンポーネントからデータを受け取ります。これは、親から子へのデータの流れを意味します。

  2. 状態(State)の管理: 各コンポーネントは、自身の内部状態(state)を持ち、この状態に基づいてUIをレンダリングします。状態が変化すると、コンポーネントは自動的に再レンダリングされます。

  3. イベントハンドリング: ユーザーのアクションやイベントは、コンポーネントの状態を変更することができます。しかし、このデータフローは常に上から下(親から子)へと流れます。

一方向データフローの利点

  • 予測可能性: データが一方向で流れることで、コンポーネント間のデータフローがより追跡しやすくなります。

  • メンテナンスの容易さ: コンポーネントの分離が促進され、コードがより管理しやすくなります。

  • バグの減少: データが一方向で流れることで、予期しない副作用やバグが減少します。

Reactでは、データバインディングは双方向ではなく、一方向であるため、データは親コンポーネントから子コンポーネントへと流れます。これにより、Reactのコンポーネントツリー内でのデータフローがより一貫性があり、予測しやすくなっています。

State

Stateと呼ばれるオブジェクトに基づいてコンポーネントを管理する。
我々はStateの値を変更し、変更された時にコンポーネントを更新する。
Stateが変更されると、Reactはそのコンポーネントを再レンダリングする。

ReactのStateは、コンポーネントの動的なデータを保持するためのものです。Stateを使うことで、コンポーネントは時間の経過とともに変化することができ、ユーザーのインタラクションやシステムのイベントに応じてUIを更新することができます。

Stateの特徴

  • ローカルまたはカプセル化: Stateはそのコンポーネント内でのみアクセス可能です。他のコンポーネントから直接アクセスされることはありません。

  • 非同期的な更新: `setState` を呼び出しても、Stateの更新とコンポーネントの再レンダリングは非同期に行われます。Reactは、提供された更新を非同期にスケジュールすることがあります。これにより、パフォーマンスが向上しますが、複数のsetState呼び出しを扱う際には注意が必要です。

  • 不変性: Stateは直接変更すべきではありません。常に `setState` または `useState` フックの更新関数を使用して更新します。つまりstateを更新する際には、直接変更するのではなく、新しいstateオブジェクトを作成してsetStateuseStateの更新関数に渡す必要があります。これにより、Reactは変更を正確に検出し、効率的な再レンダリングを行うことができます。

Stateの形式

ReactのStateは、基本的にはJSONで記述可能なオブジェクト形式であることが多いです。JSONで記述可能なオブジェクト形式とは、keyと値(文字列、数値、配列、オブジェクト)で構成され、参照および関数を含まないことです。
ReactのStateは、JSONで記述可能なオブジェクト形式とは違い、技術的には参照や関数も直接含めることができます。しかしこれは一般的な使用法としては少し異なります。参照や関数をStateに含める場合は不変性(後述)の維持に注意が必要です。
また、Stateに配列やオブジェクトを含むことは実質参照を含むことになります。

まとめると

最も簡単で望ましい:プリミティブのみで構成される

値が変わってたら変更が加わったと検出される。

  • プリミティブデータタイプ(文字列、数値、ブーリアンなど)を使用することは、Reactのstateを簡単かつ直接的に管理する最も基本的な方法です。プリミティブ値はその値自体で比較されるため、Reactはプリミティブ値のstateが変更されたときに簡単に検出し、関連するコンポーネントを適切に再レンダリングできます。

不変性に気をつければ良い:配列やオブジェクトを含む

参照が変わってたら変更が加わったと検出される。

  • 配列やオブジェクトなどの参照型データをstateに含める場合、これらは参照によって管理されます。Reactがこれらの変更を正確に検出するためには、不変性を保持することが必須です。つまり、オブジェクトや配列の内容を更新する際には、元のオブジェクトや配列を直接変更するのではなく、新しいオブジェクトや配列を作成してstateに設定する必要があります。これにより、Reactは新旧のstateの参照を比較することで変更を検出し、コンポーネントを再レンダリングできます。

できないことはないが例外的:関数を含む

  • 関数をstateに含めることは技術的に可能ですが、一般的な用途ではなく、特定のシナリオでのみ有用です。例えば、イベントハンドラや特定の動的なロジックをコンポーネント間で共有する場合などです。しかし、関数をstateとして保持するよりも、それらをコンポーネントのメソッドやフック(例:`useCallback`)として定義する方が一般的で、適切なアプローチとされています。関数がstateに含まれる場合、不変性の原則は直接適用されませんが、関数の更新や置き換えを行う場合には、新しい関数を生成してstateを更新することになります。

Stateと関数

Reactでは、ステートに関数を含めることも技術的には可能です。しかし、関数をステートに保持することは、Reactのステート管理の典型的な使用法ではありません。関数は通常、コンポーネントのメソッドとして、または`useEffect`や`useCallback`などのフックを通じて使用されます。ステートを使用してデータを追跡し、UIの更新をトリガーすることが主な目的です。

関数をステートに含める場合の例

以下は、技術的に可能であっても、一般的なパターンではない関数をステートに含める例です:

const [action, setAction] = useState(() => console.log('Hello World'));

// そして、この関数をあるイベントで実行する
<button onClick={action}>Click me</button>

このような使用法は稀であり、関数をステートに格納する代わりに、関数を直接コンポーネントのメソッドやフックとして定義する方が一般的です。Reactのステートは主にデータの状態を管理するために使用され、関数やロジックを格納するためには`useCallback`、`useEffect`、またはその他のフックが適しています。

Stateの基本的な使い方

Reactのクラスコンポーネントでは、constructor内でthis.stateを設定することでstateを初期化します。一方、フックをサポートするReact 16.8以降では、関数コンポーネント内でuseStateフックを使用してstateを宣言および管理することができます。

まず、最も基本的な例として、カウンターコンポーネントを考えてみましょう。このコンポーネントは、ボタンがクリックされるたびに数値を増加させます。

import React, { useState } from 'react';

function Counter() {
  // Stateの初期化
  const [count, setCount] = useState(0);

  // クリックイベントハンドラ
  const handleClick = () => {
    setCount(count + 1); // Stateの更新
  };

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

この時useStateに返されるsetCountはState更新関数。
setCountは値か関数を受け、setCount(count+1)は値を渡している。
また、この時のStateはcountのみをもつ。

useState

useStateの文法は

const [state, setState] = useState(initialState);

initialStateは状態の初期値です。
stateは現在の状態の値を保持する変数です。
setStateは状態を更新するための関数です。

この時、再レンダリングのトリガーになるのはsetStateの起動です。しかし重要なのは、setStateを呼び出した時に指定された新しい値(stateの更新後の値)が現在の値と異なる場合にのみ、実際に再レンダリングが行われるという点です。

複数の状態の保持

複数の状態を保持したい場合、複数のuseState宣言を使用することができます。例えば、ユーザー名と年齢を別々の状態として管理する場合は、以下のように書くことができます。

const [username, setUsername] = useState('John Doe');
const [age, setAge] = useState(30);

オブジェクトとしての状態の保持

一方で、関連する複数の値を一つの状態としてまとめて管理したい場合は、オブジェクトを使用して一つのuseStateで管理することもできます。

const [user, setUser] = useState({ name: 'John Doe', age: 30 });

この場合、user状態を更新するには、setUserを使用して新しいオブジェクトを渡す必要があります。ただし、オブジェクトを更新する際には、不変性を保つために新しいオブジェクトを作成することが重要です(例:スプレッド構文やオブジェクトの分割代入を使用する)。

不変性 (Immutability)

不変性とは、作成後にその状態を変更できないデータ構造のことを指します。Reactでは、ステートやプロップスを不変のものとして扱うことが推奨されています。つまり、オブジェクトや配列などのデータ構造を直接更新するのではなく、更新が必要な場合は新しいオブジェクトや配列を作成し、その新しい参照をステートやプロップスとして使用することが求められます。

逆に言うと、そうじゃない扱いもできてしまいます。

不変性を保ってステートを更新する方法

あたらしく配列作るの例。

// 初期ステート:配列
const [items, setItems] = useState(["item1", "item2"]);

// 新しいアイテムを追加する正しい方法
function addItem(newItem) {
  setItems([...items, newItem]); // 新しい配列を作成して更新
}

この方法では、...items(スプレッド構文)を使用してitems配列のすべての要素を新しい配列にコピーし、その新しい配列にnewItemを追加します。このプロセスでは、元のitems配列は変更されず、新しい配列がステートに設定されます。これにより、Reactは参照が変更されたことを検出し、適切にコンポーネントを再レンダリングします。

直接ステートを変更してしまう方法

既存の配列にpushの例。

// 不適切なアップデート方法
function addItemIncorrect(newItem) {
  items.push(newItem); // これは元のステートを直接変更します
  setItems(items); // 参照が変わらないため、Reactは変更を検出できない
}

この場合、items.push(newItem)により元のitems配列が直接変更されます。その後でsetItems(items)を呼び出しても、配列の参照自体は変わらないため、Reactはこれをステートの変更として検出できません。結果として、コンポーネントは再レンダリングされない可能性があります。

このアプローチは、Reactが効率的にUIを更新するために必要です。

不変性を守る理由は、Reactがステートやプロップスの変更を検出してコンポーネントを再レンダリングする際、参照の比較に依存しているからです。もしオブジェクトや配列を直接変更してしまうと、その参照は変わらないため、Reactは変更を検出できず、UIが適切に更新されません。

setState

`setState` は、Reactのクラスコンポーネントにおいて状態(state)を更新するためのメソッドです。このメソッドを使用することで、コンポーネントの状態を非同期的に更新し、更新が完了するとコンポーネントが再レンダリングされます。

つまりsetStateはそれ自体が再レンダリングのトリガーとなり得ますが、Reactの最適化の都合により、再レンダリングが不要と判断されればすっとばされることもあります。またバッチ処理によりまとめられることもあり得ます。

基本的な使用法

`setState` には主に2つの使用法があります:

オブジェクトを渡す方法: この方法では、状態を更新するための新しいオブジェクトを setState に渡します。Reactはこのオブジェクトを現在の状態にマージします。

this.setState({ someKey: 'newValue' });

関数を渡す方法: 状態の更新が現在の状態やプロップスに依存している場合、更新関数を setState に渡すことができます。この関数は現在の状態とプロップスを引数として受け取り、新しい状態オブジェクトを返します。

this.setState((prevState, props) => {
  return { someKey: prevState.someKey + props.someValue };
});

コールバック関数

this.setState({ someKey: 'newValue' }, () => {
  console.log('State has been updated and component re-rendered');
});

使用上の注意

  • 直接 `this.state` を変更するのは避けるべきです。常に `setState` を使用して状態を更新してください。

  • `setState` はバッチで処理されるため、複数の `setState` 呼び出しが同期的に行われると、最後の呼び出しのみが実行される可能性があります。

`setState` のこれらの特性を理解することは、Reactにおける状態管理とコンポーネントのライフサイクルを扱う上で非常に重要です。

this.state

Reactのクラスコンポーネントでは、`this.state`を初期化することは一般的に推奨されていますが、厳密に必須ではありません。`this.state`をコンストラクタで初期化しない場合、Reactは`this.state`が存在しないか、あるいは初期状態が`null`であると見なします。

しかし、いくつかのポイントを理解しておくことが重要です:

  1. 未定義の状態: `this.state`を初期化しない場合、最初のレンダリング時に`this.state`は`undefined`または`null`になります。これは、状態を参照する際にエラーが発生する可能性があることを意味します。例えば、`this.state.someValue`にアクセスしようとすると、`this.state`が`null`または`undefined`の場合にエラーが発生します。

  2. 後からの状態の設定: コンポーネントのライフサイクル中にいつでも`this.setState`を使用して状態を設定することができます。ただし、状態を使用する前に適切に初期化されていることを確認する必要があります。

  3. 初期化の推奨: `this.state`の初期化は、コンポーネントの状態が何であるべきかを明確にするために推奨されます。これにより、コードの可読性と保守性が向上します。

  4. コンストラクタの省略: Reactでは、コンストラクタを省略して直接クラスフィールドとして状態を宣言することもできます。これはコンストラクタを書くより簡潔で、同じ結果をもたらします。

class MyComponent extends React.Component {
  state = { /* 初期状態 */ };

  render() {
    // ...
  }
}

この方法で、コンストラクタを定義せずに状態を初期化できます。

結論として、`this.state`を明示的に初期化しないことが必ずしもエラーを引き起こすわけではありませんが、通常は明確な初期状態を設定することが推奨されます。これにより、コンポーネントの振る舞いがより予測しやすくなり、保守やデバッグが容易になります。

props

Reactでの「プロップス(props)」は、コンポーネント間でデータを渡すための方法です。プロップスは、親コンポーネントから子コンポーネントへとデータを伝達する際に使用されます。これにより、コンポーネントは再利用可能であり、その機能をさまざまなシナリオで使い分けることができます。

プロップスは基本的には読み取り専用のオブジェクトであり、コンポーネント内で直接変更することはできません。もし子コンポーネントでプロップスの値に基づいて何か状態を変更したい場合は、そのプロップスを元にローカルのstateを設定し、そのstateを変更することで対応します。

例:

// 親コンポーネント
function ParentComponent() {
  return <ChildComponent name="John" />;
}

// 子コンポーネント
function ChildComponent(props) {
  return <h1>Hello, {props.name}</h1>;
}

この例では、`ParentComponent`が`ChildComponent`に`name`というプロップスを渡しています。
JSXにおいて、いわゆるhtmlでいう属性を設定すると、子コンポーネントにpropsとして渡ります。
`ChildComponent`はこのプロップスを受け取り、それを使って表示内容を動的に変更しています。このようにプロップスを使用することで、柔軟にコンポーネントの振る舞いを親コンポーネントから制御することができます。

複数の子コンポーネントを持つ親コンポーネントのサンプルを以下に示します。この例では、ParentComponentが三つの異なるChildComponentインスタンスに異なるプロップスを渡しています。これにより、それぞれの子コンポーネントが異なるデータでレンダリングされることを示しています。

// 子コンポーネント
function ChildComponent(props) {
  return <div>Hello, {props.name}</div>;
}

// 親コンポーネント
function ParentComponent() {
  return (
    <div>
      <h2>Children:</h2>
      <ChildComponent name="John" />
      <ChildComponent name="Jane" />
      <ChildComponent name="Doe" />
    </div>
  );
}

Hooks

Reactの「フック(Hooks)」は、関数コンポーネントに状態管理やライフサイクル機能などを追加するための機能です。フックはReact 16.8で導入され、以前はクラスコンポーネントでしか利用できなかった多くの機能を、関数コンポーネントでも使えるようにしました。

つまりそれまでクラスコンポーネントをつくり、そのライフサイクルメソッド(例えばcomponentDidMount, componentDidUpdate)などでStateを変更していたものが、フックを呼び出せばStateを保持できるし、対応するフックによってStateも更新できるようになったということです。

主なフックの種類

  1. useState: コンポーネントの状態(State)を管理します。

  2. useEffect: コンポーネントのライフサイクルに関連する副作用(データの取得、購読の設定、手動でのDOMの更新など)を扱います。

  3. useContext: コンテキストAPIを使用して、親コンポーネントからの値を子コンポーネントに渡すことができます。

  4. useReducer: `useState`よりも複雑な状態ロジックを扱う際に使用します。

  5. useCallback: パフォーマンス最適化のために、コールバック関数をメモ化(再利用)します。

  6. useMemo: パフォーマンス最適化のために、計算コストの高い関数の結果をメモ化します。

  7. useRef: レンダー間で値を保持するために使用します(例: DOM要素への参照)。

例: useStateとuseEffectの使用例

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

function Example() {
  const [count, setCount] = useState(0);

  // useEffectを使用して、マウント時とカウントが変わるたびに実行される副作用を設定
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // 依存配列にcountを指定

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

この例では、`useState`を使って状態(`count`)を管理し、`useEffect`を使ってドキュメントのタイトルを更新する副作用を実装しています。依存配列に`count`を指定することで、`count`が変わるたびに副作用が実行されるようになります。

フックはReactの関数コンポーネントを強力かつ柔軟にする重要なツールです。フックを使用することで、より簡潔で読みやすいコンポーネントを作成することができます。

useEffect

useEffectフックは、副作用(side effects)を扱うために特化しています。副作用とは、データのフェッチ、購読の設定、手動でのDOMの更新など、コンポーネントのレンダリング結果に直接影響を与えない、かつコンポーネントの外部に関わる操作のことです。

例えば関数コンポーネントは以下のような形式を取ります。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

このコンポーネントはpropsを受取、JSXで記述された要素を返します。この要素はReact要素に変換されます。このReact要素はツリーとして保持され、Reactによってレンダリングされます。

この、Reactレンダリング表示プロセスに関わらないものは副作用とみなされます。

よってコンポーネントの内部の問題であるStateの更新などは副作用とは言いません。逆に、StateとPropsを用いないでデータ書き換えたらだいたい副作用です。

ただしuseEffectでStateの更新を行うことはできます。
useEffectなしで副作用の操作を関数コンポーネント内で実現することは難しく、また適切ではありません。特に以下のようなケースではuseEffectが必要です:

データのフェッチング
コンポーネントがマウントされた後に外部APIからデータを取得する場合。
データのフェッチングが副作用(Side Effect)とみなされる理由は、それがReactの純粋なレンダリングフロー(つまり、プロップスやステートの変更に基づくUIの再レンダリング)外で行われる操作だからです。Reactの主な責務は、アプリケーションの状態(state)やプロパティ(props)の変更に基づいてUIを宣言的に記述し、それに応じてUIを更新することです。このプロセスは純粋であるべきです、つまり、同じ入力に対して常に同じ出力を返す必要があります。

しかし、データフェッチングのような操作は、外部のデータソース(例えば、Web API)とのやり取りを伴います。これは次のような理由で副作用と見なされます:

  1. 非同期性: データのフェッチングは非同期操作です。つまり、APIからの応答を待っている間、コンポーネントのレンダリングは続行されます。この非同期性は、Reactの同期的なレンダリングフローとは異なる動作をします。

  2. 外部の変更に依存する: フェッチされるデータは外部のAPIやサーバーによって管理されます。このため、同じAPIリクエストでも異なるタイミングで異なるデータが返される可能性があります。これは、Reactコンポーネントの純粋な関数的な振る舞いとは異なります。

  3. 副作用の管理が必要: データフェッチングはコンポーネントのライフサイクルに影響を与える可能性があります。例えば、コンポーネントがマウントされた直後にデータをフェッチし、そのデータをコンポーネントの状態にセットします。Reactでは、このような副作用を管理するために`useEffect`フックや他のライフサイクルメソッドを提供しています。

このように、データフェッチングはReactの宣言的UIとは直接関係がないため、副作用として扱われ、適切に管理する必要があります。これにより、コンポーネントの予測可能性を保ち、バグや不整合を防ぐことができます。

購読(Subscriptions)
外部データソースへの購読を設定し、コンポーネントのアンマウント時にそれを解除する場合。
DOMの手動更新
レンダリング後にDOMを直接操作する必要がある場合。この操作はReactレンダリングプロセスをすっとばすので副作用です。
デバウンスやスロットリング
入力値の変更を監視し、一定時間後に操作を実行する場合(例:検索入力のデバウンス)。

useEffectの仕様

基本的に第一引数の関数はマウント時に一回動き、第二引数の依存配列の要素に変化があった時に動きます。

useEffectフックは、関数コンポーネント内で副作用を宣言的に実行するために使用されます。基本的な構文は以下のとおりです。

useEffect(() => {
  // 副作用を実行するコード
  return () => {
    // クリーンアップコード。オプショナル
  };
}, [dependencies]); // 依存配列。この配列内の値に変更があった場合のみ副作用が再実行される
  • 第一引数には副作用を実行する関数を渡します。この関数からは、副作用のクリーンアップ(例えば、イベントリスナーの削除など)に必要な関数をオプショナルで返すことができます。

  • 第二引数は依存配列で、この配列内の値が前のレンダー時と異なる場合にのみ、副作用が再実行されます。依存配列を空([])にすると、副作用はコンポーネントのマウント時に1回だけ実行されます。依存配列を省略すると、コンポーネントが再レンダーされるたびに副作用が実行されます。

クラスコンポーネントでのデータフェッチ例

Reactのクラスコンポーネントを使用して、`componentDidMount`, `componentDidUpdate`, `componentWillUnmount`ライフサイクルメソッドを使って副作用を扱う一般的な例を以下に示します。この例では、外部APIからデータをフェッチするシンプルなデータ取得コンポーネントを作成します。

import React, { Component } from 'react';

class DataFetcher extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      isLoading: false,
      error: null,
    };
  }

  componentDidMount() {
    // コンポーネントがマウントされたらデータをフェッチする
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    // propsが更新されたら(例えば、新しいURLからデータをフェッチする必要がある場合)、データを再フェッチする
    if (prevProps.url !== this.props.url) {
      this.fetchData();
    }
  }

  componentWillUnmount() {
    // コンポーネントがアンマウントされる前に必要なクリーンアップ(例:購読解除)
    // この例では具体的なクリーンアップは行っていませんが、実際のアプリケーションでは重要なステップになります
  }

  fetchData = () => {
    this.setState({ isLoading: true });

    fetch(this.props.url)
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => this.setState({ data, isLoading: false }))
      .catch(error => this.setState({ error, isLoading: false }));
  }

  render() {
    const { data, isLoading, error } = this.state;

    if (isLoading) {
      return <div>Loading...</div>;
    }

    if (error) {
      return <div>Error: {error.message}</div>;
    }

    return (
      <div>
        {data ? <div>{JSON.stringify(data)}</div> : <div>No data fetched</div>}
      </div>
    );
  }
}

export default DataFetcher;

このコンポーネントは、指定されたURLからデータをフェッチし、そのデータを表示します。`componentDidMount`メソッドは、コンポーネントがマウントされた直後にデータフェッチを開始するために使用されます。`componentDidUpdate`メソッドは、コンポーネントのpropsが更新されたとき(この例では特にURLが変更された場合)に再びデータフェッチを実行するために使用されます。`componentWillUnmount`メソッドは、コンポーネントがアンマウントされる前に必要なクリーンアップを行う場所です。

関数コンポーネントでのデータフェッチ例(`useEffect`使用)

同じ機能を`useEffect`フックを使用して関数コンポーネントで実現する例を以下に示します。`useEffect`フックを使用することで、クラスコンポーネントでのライフサイクルメソッドによる副作用の管理を、より宣言的に書くことができます。

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

function DataFetcher({ url }) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setData(data);
      } catch (error) {
        setError(error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [url]); // 依存配列に`url`を指定。`url`が変更されるたびに副作用を再実行します。

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {data ? <div>{JSON.stringify(data)}</div> : <div>No data fetched</div>}
    </div>
  );
}

export default DataFetcher;

この関数コンポーネントは、`useEffect`フックを使用して、コンポーネントがマウントされたときや、指定された`url`が変更されたときにデータをフェッチします。`useEffect`の依存配列(第二引数に渡される配列)に`url`を指定することで、`url`の値が変更されるたびに副作用(この場合は`fetchData`関数)が再実行されるようになります。

`useEffect`を使うことで、クラスコンポーネントの`componentDidMount`、`componentDidUpdate`、`componentWillUnmount`メソッドによる副作用の管理を、よりシンプルで再利用しやすい形で実現できます。また、副作用のロジックをコンポーネントの外に配置することで、副作用のテストが容易になるというメリットもあります。

useRef

Reactの`useRef`はReactフックの一つで、主に2つの用途があります。1つはDOM要素への参照を保持すること、もう1つはレンダー間での値の保持です。
また、useRefは再レンダリングを引き起こしません。

  1. DOM要素への参照を保持するため:`useRef`を使って作成されたrefオブジェクトを、JSX内の要素の`ref`属性に割り当てることで、その要素への直接的な参照を保持できます。これにより、DOM操作や、要素の値や状態へのアクセスが可能になります。

  2. レンダリング間での値の保持:`useRef`はレンダリング間で値を「記憶」することができるため、コンポーネントが再レンダリングされてもその値が保持されます。これはタイマーのID、外部ライブラリのインスタンスなど、レンダリングに依存しない値を保持するのに便利です。

使用方法

基本的な使用方法は以下の通りです:

import React, { useRef } from 'react';

function MyComponent() {
  // useRefを使ってrefオブジェクトを作成
  const myRef = useRef(initialValue);

  // DOM要素への参照として使用する例
  const handleClick = () => {
    if(myRef.current) {
      console.log(myRef.current.value); // inputの現在の値をログに出力
    }
  };

  return (
    <div>
      <input ref={myRef} type="text" />
      <button onClick={handleClick}>Log Value</button>
    </div>
  );
}

ここで、`initialValue`はrefオブジェクトの初期値で、`myRef.current`で現在の値にアクセスします。DOM要素への参照として使用する場合、初期値は不要ですが、値の保持に使用する場合は初期値を設定することができます。

`useRef`は`useState`と異なり、値が更新されてもコンポーネントの再レンダリングを引き起こしません。これは、パフォーマンスの最適化に役立つことがありますが、その値がアプリケーションの状態の一部として表示に影響を与える場合は、代わりに`useState`や`useReducer`を使用するべきです。

DOM要素への参照を保持

useRefを用いて実装することでよりシンプルになる例として、子コンポーネントのDOMに親コンポーネントから直接アクセスするケースを挙げてみましょう。以下は、親コンポーネントから子コンポーネントのテキスト入力をフォーカスするシンプルな例です:

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  // テキスト入力への参照を初期化
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // 明示的にテキスト入力をフォーカスする
    inputEl.current.focus();
  };

  return (
    <>
      {/* input 要素に ref として inputEl を設定 */}
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>フォーカスを当てる</button>
    </>
  );
}

export default TextInputWithFocusButton;

useRefをあんまり適当に使うと、コンポーネントの親子関係を破壊するなどしてReactの哲学に背く可能性があります。

レンダー間で値を保持

以下のサンプルコードは、useRefを使ってレンダー間で値を保持する簡単な例を示しています。この例では、ユーザーがボタンをクリックするたびにカウンターが増加しますが、このカウンターの値はコンポーネントの状態ではなく、useRefによって保持されるため、コンポーネントの再レンダーは発生しません。つまり更新を反映したければ他の再レンダリングトリガーに巻き込む必要があります。というかそうしたい時に使います。

import React, { useRef } from 'react';

function CounterExample() {
  // useRefを使用してレンダー間でカウント値を保持
  const count = useRef(0);

  const handleClick = () => {
    count.current += 1;
    console.log(`Clicked ${count.current} times`);
  };

  return (
    <div>
      <p>Check the console for the count.</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}


current`プロパティ

`current` プロパティは、Reactの `useRef` フックから来ています。`useRef` は、React コンポーネント内で参照(ref)を作成するために使用されるフックです。これにより、DOM 要素への直接的な参照を保持したり、レンダー間で値を保持することができます。

`useRef` を使用すると、返されるオブジェクトには `current` プロパティが含まれており、これが実際の参照を保持しています。この `current` プロパティは、`useRef` によって指定された初期値で初期化され、その後、参照先のDOM要素やコンポーネントのインスタンスに更新されます。

例えば、DOM 要素への参照を作成する場合:

const myElementRef = useRef(null);

ここで、`myElementRef` はオブジェクトで、その `current` プロパティは `null` で初期化されます。後に、この `ref` をあるDOM要素に割り当てることで:

<div ref={myElementRef}></div>

React がこのコンポーネントをマウントする時、`myElementRef.current` はこの `<div>` DOM 要素を指し、その後はこの要素への直接的な参照として機能します。

`InfiniteScroll` コンポーネントのコンテキストでは、`loader` という名前の `useRef` を使用しています。これは、ページの最下部に表示される要素への参照を作成し、`IntersectionObserver` がこの要素がビューポートに入る瞬間を検出できるようにするためです。`loader.current` は、監視対象となるDOM要素への参照を保持し、これにより無限スクロール機能が実現されます。

useContext

`useContext`フックはReactでコンテキスト(Context)を使いやすくするためのフックです。これにより、コンポーネントツリーを通じてデータを渡すことができ、プロップドリリング(各階層を通じて手動でpropsを渡すこと)の必要を減らします。以下は`useContext`フックの基本的な使用例です。

ステップ1: コンテキストの作成

まず、新しいコンテキストを作成します。これは通常、アプリケーションのトップレベルで行います。

import React, { createContext } from 'react';

// 新しいコンテキストを作成
const MyContext = createContext();

ステップ2: プロバイダーを使用してコンテキストの値を提供する

次に、コンテキストプロバイダー(`MyContext.Provider`)を使用して、コンテキストの値をコンポーネントツリーに渡します。プロバイダーを使用すると、そのコンテキストを購読しているすべてのコンポーネントで値にアクセスできます。

function App() {
  return (
    <MyContext.Provider value="Hello, Context!">
      {/* この中のコンポーネントはMyContextの値にアクセスできる */}
      <ChildComponent />
    </MyContext.Provider>
  );
}

ステップ3: `useContext`を使用してコンテキストの値にアクセスする

最後に、`useContext`フックを使用してコンテキストの値にアクセスします。このフックは、指定されたコンテキストオブジェクト(この例では`MyContext`)を引数として受け取り、そのコンテキストの現在の値を返します。

import React, { useContext } from 'react';

function ChildComponent() {
  // MyContextから値を受け取る
  const value = useContext(MyContext);

  return <div>{value}</div>; // "Hello, Context!" を表示
}

このサンプルでは、`App`コンポーネントが`MyContext.Provider`を使用してコンテキストの値を提供し、`ChildComponent`が`useContext`フックを使用してその値にアクセスしています。これにより、`ChildComponent`は`App`から直接プロップスを受け取らずに、コンテキストを通じてデータを取得できます。この方法は、特に大規模なアプリケーションや深いコンポーネントツリーがある場合に、コードをよりクリーンに保ち、プロップスの受け渡しを簡素化するのに役立ちます。

useReducer

`useReducer`はReactのフックの一つで、複雑なコンポーネントの状態ロジックをより管理しやすくするために使用されます。`useState`の代わりに`useReducer`を使うことで、複数のサブ値を持つ複雑な静的ロジックを扱ったり、次の状態が現在の状態に依存する場合に便利です。以下は`useReducer`フックの基本的な使用例です。

ステップ1: レデューサー関数の定義

レデューサーは、現在の状態とアクションを引数に取り、新しい状態を返す関数です。アクションは、何らかの更新を行いたいときにディスパッチ(送信)されるオブジェクトです。

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

この例では、状態はオブジェクト`{count: 0}`で、アクションは`{type: 'increment'}`または`{type: 'decrement'}`の形式を取ります。

ステップ2: コンポーネント内で`useReducer`の使用

次に、コンポーネント内で`useReducer`フックを使用して、レデューサー関数と初期状態を渡します。`useReducer`は現在の状態とディスパッチ関数をペアで返します。

import React, { useReducer } from 'react';

function Counter() {
  const initialState = { count: 0 };
  
  // useReducerを呼び出し、現在の状態とディスパッチ関数を受け取る
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

このコンポーネントでは、`-`ボタンをクリックするとカウントが減り、`+`ボタンをクリックするとカウントが増えます。各ボタンの`onClick`イベントハンドラーは、適切なアクションタイプとともに`dispatch`関数を呼び出し、レデューサー関数が新しい状態を計算し、コンポーネントがそれに応じて再レンダリングされるようにします。

`useReducer`は、特に状態の更新ロジックが複雑な場合や、複数の子コンポーネント間で状態を共有する場合に非常に便利です。また、Reduxのような状態管理ライブラリの使用に慣れている開発者にとっても、親しみやすいパターンを提供します。


クラスコンポーネント

Reactのクラスコンポーネントでは、フック(Hooks)は使用できませんが、似たような機能を実現するために「ライフサイクルメソッド」と「状態(state)」を使用します。クラスコンポーネントにおける状態管理とライフサイクルの使用例を以下に示します。

クラスコンポーネントにおける状態(State)の使用例

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    // 状態の初期化
    this.state = { count: 0 };
  }

  // クリックイベントハンドラ
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>
          Click me
        </button>
      </div>
    );
  }
}

export default Counter;

このコンポーネントでは、コンストラクタで状態を初期化し、`setState` メソッドを使用して状態を更新しています。状態が変化すると、コンポーネントは自動的に再レンダリングされます。

ライフサイクルメソッドの使用例

ライフサイクルメソッドを使用して、コンポーネントのマウント、更新、アンマウント時の振る舞いを定義します。

class Example extends Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    // コンポーネントがマウントされた後に実行
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    // 状態が更新された後に実行
    document.title = `You clicked ${this.state.count} times`;
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>
          Click me
        </button>
      </div>
    );
  }
}

ここでは、`componentDidMount` と `componentDidUpdate` ライフサイクルメソッドを使用して、コンポーネントがマウントされたときと状態が更新されたときにドキュメントのタイトルを更新しています。

クラスコンポーネントでは、これらのライフサイクルメソッドを使って、コンポーネントの生成、更新、破棄時の動作を細かく制御できます。フックが登場する以前は、これらの機能はクラスコンポーネントに固有のものでした。

ライフサイクルメソッド

Reactのクラスコンポーネントにおけるライフサイクルメソッドは、コンポーネントの作成、更新、破棄の過程で自動的に呼び出される特別なメソッドです。これらのメソッドを利用することで、特定の時点でのカスタム動作を定義できます。主に、リソースの確保や解放、データのフェッチ、手動によるDOMの更新など、コンポーネントのライフサイクルに沿った操作を行うために使用されます。

主なライフサイクルメソッド

ライフサイクルメソッドは大きくマウント、アップデート、アンマウントのフェーズに分けられます。

マウント(作成時)

  • `constructor(props)`:コンポーネントのインスタンスが作成されるときに最初に実行されます。初期状態の設定やイベントハンドラーのバインドに使用されます。

  • `static getDerivedStateFromProps(props, state)`:コンポーネントがマウントされる前、またはpropsが変更された時に呼び出されます。新しいpropsからstateを導出するために使用されます。

  • `render()`:コンポーネントのUIをレンダリングするために呼び出されます。このメソッドは純粋である必要があり、ここでは副作用を発生させてはいけません。

  • `componentDidMount()`:コンポーネントのマウントが完了した後に実行されます。DOMノードにアクセスしたり、データのフェッチ、サブスクリプションの設定などの副作用を発生させる操作に適しています。

アップデート(更新時)

  • `static getDerivedStateFromProps(props, state)`:propsの変更に応じてstateを更新する必要がある場合に使用されます。

  • `shouldComponentUpdate(nextProps, nextState)`:コンポーネントが更新される前に呼び出されます。パフォーマンス最適化のために、新しいpropsやstateを受けて再レンダリングする必要があるかどうかを決定します。

  • `render()`:コンポーネントの更新に応じてUIを再レンダリングします。

  • `getSnapshotBeforeUpdate(prevProps, prevState)`:実際のDOM変更がフラッシュされる直前に呼び出されます。DOMの現在の状態(例:スクロール位置)をキャプチャするのに役立ちます。

  • `componentDidUpdate(prevProps, prevState, snapshot)`:コンポーネントの更新が完了した後に実行されます。コンポーネントが再レンダリングされた後のDOM操作やデータのフェッチなどに使用されます。

アンマウント(破棄時)

  • `componentWillUnmount()`:コンポーネントがDOMから削除される直前に呼び出されます。サブスクリプションの解除やタイマーのクリアなど、必要なクリーンアップを行います。

注意点

  • `getDerivedStateFromProps`と`getSnapshotBeforeUpdate`は比較的新しいメソッドで、それぞれReact 16.3以降で導入されました。

  • ライフサイクルメソッドの中で最も頻繁に使用されるのは`render`, `componentDidMount`, `componentDidUpdate`, そして`componentWillUnmount`です。


この記事が気に入ったらサポートをしてみませんか?