React Logo React Hooks の基本的な使い方

基本的な React Hooks(useState、useEffect、useContext、useReducer)の使い方に関する覚書です。

作成日:2020年8月10日

関連ページ:

以下で使用している例やサンプルなどは基本的に Create React App で構築した環境で動作します。

フック (Hooks)

フック (Hooks) は React 16.8 で追加された新機能です。フックを利用するとクラスコンポーネントでしか使えなかった state などの機能を関数コンポーネントで使えるようになります。

以下は React 公式ページの「フックの導入」及び「フック早わかり」からの抜粋です。

動機
  • フックを使えば、ステートを持ったロジックを、コンポーネントの階層構造を変えることなしに再利用できます。
  • フックは関連する機能に基づいて、1 つのコンポーネントを複数の小さな関数に分割することを可能にします。
  • フックは、より多くの React の機能をクラスを使わずに利用できるようにします。

要するにフックとは?

フックとは、関数コンポーネントに state やライフサイクルといった React の機能を “接続する (hook into)” ための関数です。フックは React をクラスなしに使うための機能ですので、クラス内では機能しません。

フックは JavaScript の関数ですが、2 つの追加のルールがあります。

  • フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
  • フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください

現時点では以下のようなフックが用意されています。

  • 基本のフック
    • useState
    • useEffect
    • useContext
  • 追加のフック
    • useReducer
    • useCallback
    • useMemo
    • useRef
    • useImperativeHandle
    • useLayoutEffect
    • useDebugValue

useState ステートフック

ステートフック(useState)を使うと関数コンポーネントで state を使えるようになります。

state はコンポーネントの状態を表すプロパティで、以前はクラスコンポーネントでのみ使用することができましたが、useState を使うことにより関数コンポーネントでも state を扱えるようになりました。

以下が useState の書式で、使用する state 変数とそれを更新する関数を宣言します。

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

useState は state 変数とそれを更新するための関数を返します。

  • state:state の値が格納される変数(任意の名前を付けられます)
  • setState:state の値を更新する関数(任意の名前を付けられます)
  • initialState:state の値の初期値(初回レンダー時に返される state この値と等しくなります。)

クラスコンポーネントの state の場合

以下はボタンをクリックすると count の値が1増加するクラスコンポーネントの例です。

state は { count: 0 } から始まり、ユーザがボタンをクリックした時に this.setState() を呼ぶことで state.count を1増加します。

import React from 'react'

//クラスコンポーネント
class App extends React.Component {
  //コンストラクタ
  constructor(props) {
    super(props)
    //state の初期化
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <p>{this.state.count}  回クリックしました。</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          クリック
        </button>
      </div>
    )
  }
}

export default App;

useState フックを使う

以下は上記と同じことを useState フックを使った関数コンポーネントで記述する例です。

useState をインポート

ステートフックを使うには useState を react 本体から(名前付き)インポートする必要があります。それにより、React の state の機能を関数コンポーネントに追加できます。

//React に加え useState をインポート
import React, { useState } from 'react';

useState をインポートしない場合は、React.useState() とすれば useState() を使用することはできますが、インポートした方が useState を使っていることが明示的になります。

useState() で state 変数を宣言

クラスではコンストラクタ内で this.state を使って初期化しますが、関数コンポーネントには this は存在しないので、this.state を読み書きすることはできません。

代わりにコンポーネントの内部のトップレベルの位置で useState フックを使って state 変数を宣言します。通常、関数が終了すると変数は破棄されますが、state 変数は React によって保持されます。

フックは必ず関数コンポーネントの中のトップレベルに位置する必要があり、if 文や for 文などの中で使用することはできません。

import React, { useState } from 'react';

//関数コンポーネント
const App = () => {
  //useState を呼び出して state 変数を宣言して初期値を設定(初期化)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
    </div>
  );
}

export default App;

以下は useState を使った state 変数(とそれを更新するための関数)の宣言の書式です。

useState() の引数には state の初期値を指定します。state 変数とその関数は任意の名前を付けられますが、関数の名前は、setXxxx のように state 変数の名前の前に set を付けます。

const [state 変数, state を更新する関数] = useState(初期値);

この例の場合は、state 変数を count、更新するための関数を setCount、初期値を 0 としています。

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

useState() は state の値とそれを更新するための関数を配列として返します。初回のレンダー時に返される state の値は引数として渡された値 (初期値) と等しくなります。

この宣言は ES6 の分割代入の構文で、この例の場合、count と setCount という名前の 2 つの変数を用意して、useState から返される配列の要素の1つ目を count に、2つ目を setCount に代入しています。

以下のように記述したのと同じ意味になります(このような書き方はしませんが)。

const countState = useState(0); //2つの要素を持つ配列が返る
const count = countState[0]; //1つ目の要素は state の現在の値
const setCount = countState[1]; // 2つ目の要素はそれを更新するための関数

state の読み出し

関数コンポーネント内では、state の読み出しは直接 state 変数を使って読み出すことができます。

以下の場合、8行目の {count} で state で保存されている値を参照しています。count の値が変わるたびにレンダリングされて値が更新されます。

//関数コンポーネント
const App = () => {
  //useState を呼び出して state 変数を宣言して初期値を設定(初期化)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
    </div>
  );
}

state の更新

state を更新するには、useState() の戻り値に指定した state を更新する関数を呼び出して state を更新します(上記例では9行目)。

state を更新する関数は新しい state の値を受け取り、コンポーネントの再レンダーをスケジューリングします。

// state を更新する関数
setXxxx(新しい state の値)

上記の例の場合、ユーザがクリックすると setCount を呼び出して新しい値(count + 1)で state を更新し、React はコンポーネントを再レンダーしてその際に新たな count の値を渡します。

後続の再レンダー時には、useState から返される1番目の値(count)は常に、更新を適用した後の最新版の state になります。

useState の使用例

以下は単に div 要素と p 要素から成る関数コンポーネントです。

ステートフック useState を使ってボタンをクリックすると背景色を変更するようにしてみます。

import React from 'react';
import ReactDOM from 'react-dom';
// CSS のインポート
import './index.css';

function MyComponent() {
  return (
    <div className="content">
      <p>The background color is white.</p>
    </div>
  );
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));

背景色を変更するので、以下のような CSS を用意して import で読み込みます。Create React App を使っている場合は、上記のように import で CSS ファイルを指定すれば、webpack により適切な位置に CSS が読み込まれます。

この時点では div 要素に .content が指定されているだけなので、幅とパディングだけが適用されています。後でクラス(.bg-white または .bg-black)を使って背景色のスタイルを適用するようにします。

index.css
.content {
  width: 100%;
  height: 100%;
  padding: 20px;
}

.bg-white {
  background-color: #fefefe;
  color: #666666;
}

.bg-black {
  background-color: #333333;
  color: #eeeeee;
}

useState をインポートします。そして useState を使って、state 変数(isDark)とその値を更新する関数(setDark)を宣言し、useState() の引数に state 変数の初期値(false)を設定します。

この例の場合、state 変数の isDark は背景色が暗い色かそうでないかを意味する値で true または false の真偽値を取ります。初期値は false(背景色は暗くない)を指定しています。

// useState の名前付きインポートを追加
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function MyComponent() {
  // state 変数とそれを更新する関数を宣言し、state 変数の初期値を設定
  const [isDark, setDark] = useState(false);
  
  return (
    <div className="content">
      <p>The background color is white.</p>
    </div>
  );
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));

三項演算子を使って isDark の値により black または white の値を取る変数 bgColor を作成します。

className={`content bg-${bgColor}`} は ES6 のテンプレートリテラルを使った記述で、変数をその中に展開することができます。例えば、bgColor が black の場合は className="content bg-black" に変換されます。

function MyComponent() {
  const [isDark, setDark] = useState(false);
  // isDark の値により black または white の値を取る変数
  const bgColor = isDark ? "black" : "white";
  
  // className と 文字の一部に上記で設定した変数を指定
  return (
    <div className={`content bg-${bgColor}`}>
      <p>The background color is {bgColor}.</p>
    </div>
  );
}

上記の状態では最初の状態と変化はありませんが、以下のように useState(true) として初期値を変更すると isDark は true になるので、bgColor は "black" になり、.bg-black のスタイルが適用され、表示される文字列は The background color is black. になります。

// 初期値を true に変更すると
const [isDark, setDark] = useState(true);
// isDark は true になるので bgColor は "black" となる
const bgColor = isDark ? "black" : "white";

state はコンポーネントの状態を表すプロパティで、state の値が更新されると React はそれを検知してコンポーネントの render() メソッドが実行される仕組みになっています。

ボタンをクリックして state を変更して背景色を切り替えられるように以下のようなボタンを追加します。

<button onClick={() => setDark(!isDark)}>
  Change to {isDark ? "white" : "black"}
</button>

ボタンにはイベントハンドラを設定して、クリックされたら state を更新する関数 setDark() を呼び出して新しい state の値を !isDark として現在の値を反転させます。

React ではイベントは onClick のようにキャメルケースになります。(関連:イベント処理

また、ボタンに表示する文字列は isDark の値を基に切り替わるようにしています。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

function MyComponent() {
  const [isDark, setDark] = useState(false);
  const bgColor = isDark ? "black" : "white";
  
  //ボタンを追加
  return (
    <div className={`content bg-${bgColor}`}>
      <p>The background color is {bgColor}.</p>
      <button onClick={() => setDark(!isDark)}>
        Change to {isDark ? "white" : "black"}
      </button>
    </div>
  );
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));

イベントハンドラは以下のように別途定義して onClick にイベントハンドラを指定することもできます。

function MyComponent() {
  const [isDark, setDark] = useState(false);
  const bgColor = isDark ? "black" : "white";
  
  //イベントハンドラを別途定義
  const handleClick = () => {
    setDark(!isDark);
  }
  
  return (
    <div className={`content bg-${bgColor}`}>
      <p>The background color is {bgColor}.</p>
      <button onClick={handleClick}>
        Change to {isDark ? "white" : "black"}
      </button>
    </div>
  );
}

以下は前述の例のボタン部分を別のコンポーネントとして抽出した例です。

子コンポーネントの MyButton は直接親コンポーネント MyComponent の state を変更できないので、親コンポーネントで定義した関数を props 経由で受け取りイベントハンドラに設定します。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';

//ボタンをコンポーネントとして抽出
function MyButton({OnBtnClick, isDark}) {
  //イベントハンドラには props 経由で受け取る MyComponent の関数 を設定
  return (
    <button onClick={OnBtnClick}>
      Change to {isDark ? "white" : "black"}
    </button>
  );
}

function MyComponent() {
  const [isDark, setDark] = useState(false);
  const bgColor = isDark ? "black" : "white";
  
  // MyButton に渡して state を更新する関数
  const handleOnClick = () => {
    setDark(!isDark);
  }
  
  //props.OnBtnClick で MyButton へイベントハンドラを渡す
  return (
    <div className={`content bg-${bgColor}`}>
      <p>The background color is {bgColor}</p>
      <MyButton OnBtnClick={handleOnClick} isDark={isDark}/>
    </div>
  );
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));

上記3行目の MyButton では props を分割代入で受け取っていますが、以下のように記述したのと同じ意味になります。

function MyButton(props) {
  return (
    <button onClick={props.OnBtnClick}>
      Change to {props.isDark ? "white" : "black"}
    </button>
  );
}

フォームで useState を使う

以下はクラスコンポーネントでフォームを使う例です。

フォーム要素の value 属性に state プロパティを設定し、onChange イベントで setState() を呼び出して state プロパティを更新することで value 属性を更新します。(関連:制御されたコンポーネント

import React from 'react';
import ReactDOM from 'react-dom';
 
class MyForm extends React.Component {
  // コンストラクタ
  constructor(props) {
    super(props);
    // state プロパティの初期化(state.value の初期値の設定)
    this.state = {value: ''};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  // onChange イベントのハンドラ
  handleChange(event) {
    // ユーザがタイプする度に state を入力される値で更新 → value 属性も更新される
    this.setState({value: event.target.value});
  }

  // onSubmit イベントのハンドラ
  handleSubmit(event) {
    // 送信ボタンがクリックされたらその時点での state をコンソールに出力
    console.log(this.props.name + ': ' + this.state.value);
    //デフォルトの動作(フォームの送信)を抑止
    event.preventDefault();
  }

  render() {
    // value 属性の値に state.value を指定して state を関連付け
    // onChange イベントで value 属性の値を随時更新
    return (
      <form onSubmit={this.handleSubmit}>
      <label>
      {this.props.name}: 
        <input type="text" value={this.state.value} onChange={this.handleChange} />
      </label>
      <input type="submit" value="送信" />
      </form>
    );
  }
}

ReactDOM.render(
  <MyForm name="お名前"/>,
  document.getElementById('root')
);

以下は上記を関数コンポーネントで useState を使って書き換えた例です。

基本的にフォームの使い方はクラスコンポーネントの場合と同じです。但し、クラスで使う state が使えないので、代わりに useState を使って処理します。

クラスコンポーネントに比べ、コンストラクタやメソッドのバインドが不要な分、簡潔に記述できます。

import React, { useState} from 'react';
import ReactDOM from 'react-dom';

function MyForm(props) {
  //useState を呼び出して state 変数(val)を宣言して初期値を設定
  const [val, setVal] = useState('');
 
  // onChange イベントのハンドラ
  function handleChange(event) {
    // ユーザがタイプする度に setVal で state の値 val を入力値で更新 
    setVal(event.target.value);
  }
 
  // onSubmit イベントのハンドラ
  function handleSubmit(event) {
    // 送信ボタンがクリックされたらその時点での state の値 val をコンソールに出力
    console.log(props.name + ': ' + val);
    //デフォルトの動作(フォームの送信)を抑止
    event.preventDefault();
  }
 
  // value 属性の値に  state の val を指定して state を関連付け
  // onChange イベントで value 属性の値を随時更新
  return (
    <form onSubmit={handleSubmit}>
    <label>
    {props.name}: 
      <input type="text" value={val} onChange={handleChange} />
    </label>
    <input type="submit" value="送信" />
    </form>
  );
}
 
ReactDOM.render(
  <MyForm name="お名前"/>,
  document.getElementById('root')
);

以下は同じことをアロー関数を使って記述した場合の例です。

const MyForm = props => {
  //useState を呼び出して state 変数(val)を宣言して初期値を設定
  const [val, setVal] = useState('');
  
  // onChange イベントのハンドラ(ユーザがタイプする度に setVal で state の値を入力値で更新)
  const handleChange = event => setVal(event.target.value);
  
  // onSubmit イベントのハンドラ
  const handleSubmit = event => {
    // 送信ボタンがクリックされたらその時点での state の値 val をコンソールに出力
    console.log(props.name + ': ' + val);
    //デフォルトの動作(フォームの送信)を抑止
    event.preventDefault();
  }
 
  // value 属性の値に  state の val を指定して state を関連付け
  // onChange イベントで value 属性の値を随時更新
  return (
    <form onSubmit={handleSubmit}>
    <label>
    {props.name}: 
      <input type="text" value={val} onChange={handleChange} />
    </label>
    <input type="submit" value="送信" />
    </form>
  );
}

複数の state 変数を使う

複数の state 変数を使う場合は、複数の useState を使います。

function ExampleWithManyStates() {
  // 複数の state 変数を宣言(複数の useState を使う)
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  ・・・

上記の場合、ローカル変数として age、fruit、todos があり、それぞれを個別に更新することができます。

function handleOrangeClick() {
  // useState() で指定した関数で個別に更新
  setFruit('orange');
}

state 変数はオブジェクトや配列も保持できるので、関連する値をいっしょにまとめておくこともできます。

但し、クラスでの this.setState とは異なり、state 変数の更新はマージではなく必ず古い値を置換します。

以下はフォームで複数の useState を使う例です。

import React, { useState} from 'react';
import ReactDOM from 'react-dom';

const MyCalculatorForm = props => {
  //複数の useState を使って変数と関数を宣言
  const [input1, setInput1] = useState(0);
  const [input2, setInput2] = useState(0);
  const [operator, setOperator] = useState('+');
  
  //それぞれを個別に更新
  const handleChange1 = event => {
    setInput1(event.target.value);
  }
  const handleChange2 = event => {
    setInput2(event.target.value);
  }
  const handleChangeOperator = event => {
    setOperator(event.target.value);
  }
  
  let result = 0;

  if(operator === '+') {
     result = parseFloat(input1) +  parseFloat(input2);
  }else{
    result =  parseFloat(input1) -  parseFloat(input2);
  }

  return (
    <div>
      <select name="operator" value={operator} onChange={handleChangeOperator}>
        <option value="+"> 加算 </option>
        <option value="-"> 減算 </option>
      </select>
      <br/>
      <input type="text" value={input1} onChange={handleChange1} size="10" />
      <input type="text" value={input2} onChange={handleChange2}  size="10" />
      <br/>
      result: <span> {result} </span>
    </div>
  );
}
 
ReactDOM.render(
  <MyCalculatorForm />,
  document.getElementById('root')
);

以下は上記の2つの input 要素をまとめて1つの state オブジェクトとして値を保持して更新する例です。

input1 と input2 を1つのオブジェクトとしてまとめることでクラスコンポーネントと同じように、それぞれの入力要素に name 属性を指定して、 event.target.name に基づいて1つのイベントハンドラで処理をすることができます(複数の入力の処理)。

※この例の場合、2つの input 要素には関連性があるので1つにまとめても問題はありませんが、関連性のない状態は個別に useState で管理します。

値を更新する際は、スプレッド構文 (...) を使って既存のオブジェクト(input)をコピーし、算出プロパティ ([ ]) を使ってイベントの発生した項目を追加することでその項目を上書きして、新しい値(オブジェクト)として渡しています。

const MyCalculatorForm = props => {

  //input1 と input2 を1つのオブジェクトとしてまとめる
  const [input, setInput] = useState({
    input1: 0,
    input2: 0
  });
  
  const [operator, setOperator] = useState('+');
  
  //input1 と input2 をまとめて更新
  const handleChange = event => {
    //オブジェクトのスプレッド構文(...)と算出プロパティ [ ] を使用
    setInput({
      //既存のオブジェクトをコピー
      ...input,  
      //イベントの発生した項目(input1 または input2)
      [event.target.name]: event.target.value 
    });
  };
  
  const handleChangeOperator = event => {
    setOperator(event.target.value);
  }
  
  let result = 0;

  if(operator === '+') {
     result = parseFloat(input.input1) +  parseFloat(input.input2);
  }else{
    result =  parseFloat(input.input1) -  parseFloat(input.input2);
  }

  return (
    <div>
      <select name="operator" value={operator} onChange={handleChangeOperator}>
        <option value="+"> 加算 </option>
        <option value="-"> 減算 </option>
      </select>
      <br/>
      <input name="input1" type="text" value={input.input1} onChange={handleChange} size="10" />
      <input name="input2" type="text" value={input.input2} onChange={handleChange}  size="10" />
      <br/>
      result: <span> {result} </span>
    </div>
  );
}

以下は state を配列で保持する例です。

ボタンをクリックすると input 要素に入力された文字列をリストに追加します。リストの項目は配列で保持します。(関連:リストと key

state を更新する関数(setInputs)では古い値とマージはせずに、新しい値で上書きします。配列に新しい項目を追加するにはスプレッド構文 (...) を使って既存の項目(inputs)を新しい配列にコピーし、新しい項目を末尾に挿入して生成した新しい配列を渡しています。

import React, {useState} from 'react';
import ReactDOM from 'react-dom';
 
const InputsList = () =>  {
  //input 要素に入力される値
  const [val, setVal] = useState('');
  //入力された内容のリストを空の配列 [] で初期化
  const [inputs, setInputs] = useState([]);
  
  // onChange イベントのハンドラ
  function handleChange(event) {
    // ユーザがタイプする度に setVal で state の値 val を入力値で更新 
    setVal(event.target.value);
  }

  const addInput = () => {
    //inputs を更新する関数を呼び出し
    setInputs([
      //既存の項目を新しい配列にコピー
      ...inputs,
      //新しい項目を末尾に挿入
      {
        id: inputs.length,
        value: val
      }
    ]);
    //入力フィールドをクリア
    setVal('');
  };

  return (
    <>
      <label>
      入力内容: 
        <input type="text" value={val} onChange={handleChange} />
      </label>
      <button onClick={addInput}>入力内容を追加</button>
      <ul>
        {inputs.map(input => (
          <li key={input.id}>{input.value}</li>
        ))}
      </ul>
    </>
  );
}
 
ReactDOM.render(
  <InputsList />,
  document.getElementById('root')
);

関数型の更新

新しい state が前の state に基づいて計算される場合は、更新するための関数(以下では setCount)に関数を渡すことができます。この関数は前回の state の値を受け取り、更新された値を返します。

//更新するための関数で前回の state の値 prevCount を受け取り
setCount(function(prevCount) {
  //更新された値を返す(この例では前回の state の値から1を引いた値を返す)
  return prevCount - 1;
})

//アロー関数で記述する場合
setCount(prevCount => prevCount - 1)

以下は、更新するための関数の両方の形式を用いたカウンタコンポーネントの例です。

+ と - のボタンは更新後の値が更新前の値に基づいて計算されるため、関数形式を使い、Reset ボタンは常にカウントを初期値(props 経由で渡される initialCount)に戻すので、通常の形式を使っています。

import React, { useState} from 'react';
import ReactDOM from 'react-dom';

function MyCounter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  );
}

ReactDOM.render(
  <MyCounter initialCount={0} />, 
  document.getElementById('root')
);

更新用関数が現在の state と全く同じ値を返す場合は、後続する再レンダーは完全にスキップされます。

state の遅延初期化

useState() に指定する引数(初期値)は初回レンダー時にのみ使われる state の値で、後続のレンダー時にはその値は無視されます。

もし初期値の state が時間のかかる計算をして求める値である場合は、代わりに関数を渡すことができます。この関数は初回のレンダー時にのみ実行されます。

//初期値の代わりに関数を渡すことができる
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

レンダリングについて

それぞれのレンダーは固有の props と state を保持している

最初のサンプルに使った以下のコードの8行目の count はどのように更新されるのでしょうか。

import React, { useState } from 'react';

const MyCounter = () => {
  const [count, setCount] = useState(0);  //初期値を設定
 
  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
    </div>
  );
}

ReactDOM.render(
  <MyCounter />,
  document.getElementById('root')
)

count は state の変更を検知して自動的にアップデートされているわけではなく、この場合 count は単なる数値です。

コンポーネントが一番初めにレンダリングする際、useState() から出力される count 変数の値は初期値の 0 です。

ボタンがクリックされると、setCount(count + 1) が実行され state が更新され、React はコンポーネント関数を再度呼び出し、その際、count は1となります。

state を更新するたびに、React はコンポーネント関数を呼び出します。 そして各レンダー結果は(コンポーネント関数内の定数である) state の値を「認識」します。

以下の行は特別なデータバインディングを行っているわけではありません。

<p>{count} 回クリックしました。</p>

上記は各レンダー結果に数値を組み込んでいるだけで、この数値は React が提供しています。

setCount が実行されると、React は新しい count の値を使ってコンポーネントを呼び出して、レンダー結果にマッチするよう DOM をアップデートします。

重要なポイントは、特定のレンダー内の count は時間の経過とともに変化しないことです。state の値が更新されると、再度呼び出されるのはコンポーネントであり、各レンダーはレンダー間で分離された独自の count の値を参照します。つまり、count の値はコンポーネント関数への特定の呼び出しごとに一定であり、呼び出されるコンポーネント関数ごとに保持されています。

それぞれのレンダーは固有のイベントハンドラを保持している

以下は Click ボタンをクリックすると count の値を1増加して表示し、Console ボタンをクリックすると、その3秒後に count の値をコンソールに出力します。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const MyCounter = () => {
  const [count, setCount] = useState(0);  //初期値を設定
  
  function handleClick() {
    setTimeout(() => {
      console.log("クリックした際のカウント: " + count);
    }, 3000);
  }
 
  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>Click</button>
      <button onClick={handleClick}>Console</button>
    </div>
  );
}

ReactDOM.render(
  <MyCounter />,
  document.getElementById('root')
)

例えば、Click ボタンを3回クリックして count を3まで増やして、その時点で Console ボタンをクリックし、タイムアウトになる前(3秒以内)に更に4回 Click ボタンをクリックして count を7にします。

すると、コンソールには Console ボタンをクリックしてから3秒後に「クリックした際のカウント: 3」とコンソールに出力されます。

コンポーネント関数はレンダリングごとに1回呼び出されますが、その中の count の値はすべて一定で、特定の値としてそのレンダーの state に設定されます。

各レンダーはイベントハンドラの独自のバージョン(固有のイベントハンドラ)を返します。 そしてこれらのバージョンはそれぞれ独自の count を保持しています。

count は特定のレンダーに属しています。count はスコープ内の変数であるため、イベントハンドラーはそれらが属しているレンダリングから count を参照します。

レンダーフェーズとコミットフェーズ

概念的に、React はレンダーとコミットの2つのフェーズで動作します。

レンダーフェーズ
変更対象(例えば DOM)にどのような変更が必要か決めます。このフェーズにおいて、React は render を呼び出し、1 つ前のレンダー結果と比較します。
コミットフェーズ
React は変更を反映します(React DOM の場合ではここで React は DOM ノードの挿入、更新、削除を行います)。React はこのフェーズで componentDidMount や componentDidUpdate などのライフサイクルの呼び出しも行います。

useEffect 副作用フック

useEffect は DOM の操作やデータの取得(API との通信)、タイマー、ロギングなどの副作用(side effect)を実行するためのフックです。

DOM の書き換えやデータの取得、タイマー、ロギング、あるいはその他の副作用を、関数コンポーネントの本体(React のレンダーフェーズ)で書くことはできません。それを行うと UI にまつわるバグや非整合性を引き起こしてしまうため、代わりに useEffect を使います。

useEffect に指定した処理(関数)は、コンポーネントがレンダリングされた直後に実行されます。

デフォルトでは useEffect に指定した関数はレンダーが終了した後に毎回動作しますが、第2引数(依存変数)を使って特定の値が変化した時のみ動作させるようにすることもできます。

useEffect を使うには useState と同様、 useEffect を react 本体から(名前付き)インポートする必要があります。

//React に加え useEffect をインポート
import React, { useEffect } from 'react';

コンポーネントのトップレベルの位置で宣言を行い、第1引数に実行したい処理(関数)の定義を渡します。この第1引数に渡した関数を副作用(関数)と呼ぶことがあります。

必要に応じてクリーンアップするための関数(クリーンアップ関数)を返すことができます。

第2引数には依存変数を配列で指定することができます(依存変数に変更があった場合にのみ指定した処理が実行されます)。第2引数を省略すると、指定した関数はレンダリング後に毎回実行されます。

useEffect(() => {
  // レンダリング後に実行したい処理(副作用)を記述
  // return クリーンアップ関数(オプション)
},[依存変数])
function 文の場合
useEffect(function() {
  // レンダリング後に実行したい処理(副作用)を記述
  // return クリーンアップ関数(オプション)
},[依存変数])

例えば以下の場合、レンダリングされる前に DOM 要素を取得しようとしているので DOM 上には要素は存在しないため elem は null になります。

import React from 'react';
import ReactDOM from 'react-dom';

function MyMessage() {

  //ここではレンダリングされる前なので、DOM 要素を取得できない
  const elem = document.getElementById('msg'); 
  console.log(elem); //null が出力される
 
  return <h1 id="msg">My Message </h1>
}

ReactDOM.render(<MyMessage />, document.getElementById('root'));

クラスコンポーネントであればコンポーネントのインスタンスが作成されて DOM に挿入された直後に呼び出されるライフサイクルメソッド componentDidMount() を使って、以下のようにすれば DOM 要素を取得できますが、関数コンポーネントではライフサイクルメソッドは使えません。

import React from 'react';
import ReactDOM from 'react-dom';

//クラスコンポーネント
class MyMessage extends React.Component {

  //コンポーネントがマウントされた直後に呼び出されるメソッド
  componentDidMount(){
    //DOM 挿入後に呼び出されるので DOM 要素を取得できる
    const elem = document.getElementById('msg');
    //以下は <h1 id="msg">My Message </h1> と出力
    console.log(elem); 
  }
  
  render() {
    return <h1 id="msg">My Message </h1>;
  }
  
}

ReactDOM.render(<MyMessage />, document.getElementById('root'));

useEffect を使うと、記述した処理がコンポーネントのレンダリング後に実行されるので DOM 要素を取得することができます。

//useEffect をインポート
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';

function MyMessage() {

  useEffect(() => {
    //useEffect を使うとレンダリング後に実行されるので DOM 要素を取得できる
    const elem = document.getElementById('msg');
    console.log(elem); //<h1 id="msg">My Message </h1> と出力
  });

  return <h1 id="msg">My Message </h1>;
  
}

ReactDOM.render(<MyMessage />, document.getElementById('root'));

以下の state の値を更新する関数 setVal() の実行は無限ループを引き起こします。

この場合、setVal が実行されると state の値が更新されて再レンダリングされるので MyComponent が実行され、また setVal が実行されて・・・と無限ループになってしまいます。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function MyComponent() {

  const [val, setVal] = useState('');
  // ここで setVal を呼び出して state の値を更新すると無限ループになる
  setVal('Foo');
  console.log(val);
  
  return <p>{val}</p>;
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
)

useEffect を使うと指定した処理がコンポーネントのレンダリング後に実行されるので、以下の場合、無限ループにはなりません。

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function MyComponent() {
  
  const [val, setVal] = useState('');
  
  //useEffect 
  useEffect(() => {
    setVal('Foo');
    console.log(val);
  });
  
  return <p>{val}</p>;
}

ReactDOM.render(
  <MyComponent />,
  document.getElementById('root')
)

useEffect フックを使った例

以下は useEffect を使って、初回レンダー時とボタンをクリックした際に、クリック回数を含んだメッセージをドキュメントのタイトルにセットする例です。

useEffect を使うことで、レンダー後に指定した処理(副作用関数)を実行しなければならないことを React に伝えます。

副作用関数は関数スコープ内にあるため、副作用関数内から state である count(や任意の props)にアクセスできるようになります。

React がコンポーネントをレンダーする際に React はこの副作用関数を覚えておき、DOM を更新した後に呼び出します。デフォルトでは、副作用関数は初回のレンダー時および毎回の更新時に呼び出されます。React は副作用関数が実行される時点では DOM が正しく更新され終わっていることを保証します。

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const MyCounter = () => {
  const [count, setCount] = useState(0);

  // useEffect フック
  useEffect(() => {
    // document title を更新(ドキュメントのタイトルにメッセージをセット)
    document.title = `${count} 回クリックしました。`;
  });

  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
    </div>
  );
}

ReactDOM.render(
  <MyCounter />,
  document.getElementById('root')
)

useEffect に渡される副作用関数は毎回のレンダーごとに異なっています。言い換えると、それぞれの副作用関数は特定の1回のレンダーと結びついて、特定のレンダー内の count の値を参照することができます。

このため副作用はレンダー結果の一部のようにふるまうようになります。

// 初回のレンダー
const MyCounter = () => {
  // ...
  useEffect(
    // 初回のレンダーの副作用関数
    () => {
      document.title = `${0}  回クリックしました。`;
    }
  );
  // ...
}

// クリックされると、コンポーネント関数が呼び出される
const MyCounter = () => {
  // ...
  useEffect(
    // 2回目のレンダーの副作用関数
    () => {
      document.title = `${1}  回クリックしました。`;
    }
  );
  // ...
}

// 再度クリック後、またコンポーネント関数が呼び出される
const MyCounter = () => {
  // ...
  useEffect(
    // 3回目のレンダーの副作用関数
    () => {
      document.title = `${2}  回クリックしました。`;
    }
  );
  // ..
}

それぞれのレンダーで以下のようなことが起きています。

初回のレンダー

  • React: state が 0 の時の UI を教えて。
  • コンポーネント:
    • これがレンダー結果だよ: <p>0 回クリックしました。</p>
    • 終わった後にこの副作用関数を実行してね: ( ) => { document.title = '0 回クリックしました。' }
  • React: 了解。UI をアップデート中。ブラウザ、DOM を変更したよ。
  • ブラウザ: すばらしい。画面に描画したよ。
  • React: OK。今から与えられた副作用を実行するね。
    • ( ) => { document.title = '0 回クリックしました。' }を実行中

2回目のレンダー

  • ボタンがクリックされて state が更新される。
  • コンポーネント: React、 state を 1 にセットして。
  • React: state が 1 の時の UI を教えて。
  • コンポーネント:
    • これがレンダー結果だよ: <p>1 回クリックしました。</p>
    • 終わった後にこの副作用関数を実行してね: ( ) => { document.title = '1 回クリックしました。' }
  • React: 了解。UI をアップデート中。ブラウザ、DOM を変更したよ。
  • ブラウザ: すばらしい。画面に描画したよた。
  • React: OK。今からこのレンダーに属する副作用を実行するね。
    • ( ) => { document.title = '1 回クリックしました。' }を実行中

引用元:useEffect完全ガイド(それぞれの render は独自のエフェクトを保持している)

クラスを使った例

以下はクラスを使って前述の例と同じこと(初回レンダー時とボタンをクリックした際に、クリック回数を含んだメッセージをドキュメントのタイトルにセット)を行う例です。

この場合、コンポーネントがマウント直後なのか更新後なのかに関係なく(毎回のレンダー時に)同じ副作用を実行したいので componentDidMount と componentDidUpdate の2つのライフサイクルメソッドに同じ処理を記述しています。

import React from 'react';
import ReactDOM from 'react-dom';

class MyCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  
  //コンポーネントがマウントされた直後に呼び出されるメソッド(初回レンダー時)
  componentDidMount() {
    document.title = `${this.state.count} 回クリックしました。`;
  }
  
  //更新が行われた直後に呼び出されるメソッド(ボタンがクリックされて state が更新された時)
  componentDidUpdate() {
    document.title = `${this.state.count} 回クリックしました。`;
  }

  render() {
    return (
      <div>
        <p>{this.state.count}  回クリックしました。</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          クリック
        </button>
      </div>
    );
  }
}

ReactDOM.render(
  <MyCounter />,
  document.getElementById('root')
)

条件付きで副作用を実行(依存変数)

デフォルトの動作では、副作用関数はレンダーの完了時に毎回実行されますが、第2引数を指定することによって、実行される条件を制御することができます。

以下は前述の例に、useState を使って random という state を追加して、この値を更新することでコンポーネントを再レンダーするボタンを追加しています。

random は単に値を更新して再レンダーを発生させるだけの state で、その値は使用されていません。

import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const MyCounter = () => { 
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `${count} 回クリックしました。`;
    //副作用関数が実行された際にコンソールに出力
    console.log("タイトル更新");
  });
  
  //レンダリング(再レンダー)を発生させるための乱数
  const [random, setRandom] = useState(Math.random());

  // レンダリングボタンをクリックすると random を更新(レンダーされる)
  const reRender = () => {
    setRandom(Math.random());
    console.log("再レンダー");
  }
 
  return (
    <div>
      <p>{count} 回クリックしました。</p>
      <button onClick={() => setCount(count + 1)}>
        クリック
      </button>
      <button onClick={reRender}>
        レンダリング
      </button>
    </div>
  );
}
 
ReactDOM.render(
  <MyCounter />,
  document.getElementById('root')
)

レンダリングとあるボタンをクリックすると、イベントハンドラ reRender が実行されて random の値が更新されるのでコンポーネントが再レンダーされます。

副作用関数はレンダーの完了時に毎回実行されるので、タイトルの更新も毎回実行されてしまいます。

以下はクリックとあるボタンを2回クリックして count を2に増やし、その後レンダリングとあるボタンを3回クリックした場合のコンソールの出力です。count が増えていないので、タイトルを更新する必要はありませんが、タイトルの更新も毎回実行されています。

useEffect のオプションの第2引数として配列を渡すことで、再レンダー間で特定の値が変わっていない場合には副作用の適用をスキップするように React に伝えることができます。

言い換えると、指定した値に変更があった場合のみ副作用を実行させることができます。

この例の場合は、count が変更されていない場合は、副作用を実行する必要がないので、以下のように第2引数の配列に count を指定します。

//依存変数を第2引数の配列に指定 
useEffect(() => {
  document.title = `${count} 回クリックしました。`;
  console.log("タイトル更新");
  //count が変わっていない場合は副作用をスキップ
}, [count]);

上記の依存変数(配列)の追加により、count が変わっていない場合はタイトル更新は行われなくなります(count が変わった場合のみタイトルの更新が行われます)。

もしも副作用とそのクリーンアップを1度だけ(マウント時とアンマウント時にのみ)実行したいという場合、空の配列 [ ] を第2引数として渡すことができます。

空の配列 [ ] を渡した場合、副作用内では props と state の値は常にその初期値のままになります。

この例の場合、空の配列 [ ] を渡すとタイトルはマウント時の「0 回クリックしました。」のまま、変更されなくなります。

また、「React Hook useEffect has a missing dependency: 'count'. Either include it or remove the dependency array react-hooks/exhaustive-deps(依存変数が指定されていません)」のような Warning がコンソールに表示されます。

第2引数の指定方法により、以下のようなパターンがあります。

  • レンダリング後に毎回必ず実行(第2引数を省略)
  • 初回のレンダリング後だけ実行(空の配列を指定)
  • 指定した値に変更があった場合のみ実行(配列に依存変数を指定)

props が変更された時に副作用を実行

今までの例は、state が変更された時に副作用を実行するものでしたが、同様に props が変更されたときも useEffect を使って副作用を実行することができます。

以下の例では PropWatch コンポーネントは2つのプロパティ(pet と count)を受け取り、それらが変更された時にそれぞれの副作用(コンソールへの出力)を実行します。

props の pet は App コンポーネントのセレクトボックスで選択された値です。count はボタンをクリックしたら1ずつ増加する値で、props 経由で PropWatch コンポーネントに渡され、値が変更されると useEffect を使ってそれらの値をコンソールに出力します。

第2引数に依存変数を指定しているので、それぞれが変更された場合にのみ出力するようになっています。

また、以下では useEffect フックを2つ使用していますが、フックへの呼び出しの順番がレンダー間で変わらない限り、1つのコンポーネント内で複数の state や副作用を使うことができます(フックのルール)。

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function PropWatch({ pet, count }) {
  
  //props の pet が変更された場合に実行する副作用
  useEffect(() => {
    console.log(`pet が ${pet} に変更されました`);
  }, [pet]);
  
  //props の count が変更された場合に実行する副作用
  useEffect(() => {
    console.log(`count が ${count} に変更されました`);
  }, [count]);

  return (
    <div>
     <p>props の値:pet = {pet}  count = {count}</p>
    </div>
  );
}

function App() {
  
  //select 要素の value 属性に state を設定
  const [pet, setPet] = useState('猫');
  //select 要素のイベントハンドラ
  const handleChange = event => {
    setPet(event.target.value);
  }
  //ボタンをクリックすると setCount で 1増加する値
  const [count, setCount] = useState(0);

  return (
    <div>
      <PropWatch pet={pet} count={count} />
      <select value={pet} onChange={handleChange}>
        <option value="猫">Cat</option>
            <option value="犬">Dog</option>
            <option value="うさぎ">Rabit</option>
            <option value="魚">Fish</option> 
      </select>
      <button onClick={() => setCount(count + 1)}>Count</button>
    </div>
  );
}
ReactDOM.render(
  <App />,
  document.querySelector("#root")
);

データの取得

コンポーネントが読み込まれた(マウントされた)後に、外部からデータを取得する場合などは useEffect を利用することができます。

以下は非同期通信でレスポンスを取得する関数 fetch() メソッドを使って取得した外部のデータをレンダリングする例です。

外部のデータは JSONPlaceholder(Fake online REST API for developers)を利用しています(JSONPlaceholder はサインアップなしで利用できる API です)。

useState を使って state 変数 posts と更新関数 setPosts を定義し、posts を空の配列で初期化します。レンダー後、useEffect で返す関数の中の fetch() メソッドで外部データを取得して、setPosts で取得したデータで posts を更新します。

※第2引数の依存変数の配列を適切に指定する必要があります。第2引数に何も指定しないと、setPosts を実行して state を更新した際にコンポーネントの再レンダリングが起こり、useEffect 内の処理が再実行されてしまうので、フェッチが継続して行われてサーバへの負荷をかけてしまいます。

依存変数の配列には setPosts を渡します(または空の配列を渡します)。useState を呼び出すと更新関数 setPosts 変数は一度だけ作成されて返されるので、副作用は1回だけ実行されます。

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function MyPosts() {
  // state 変数 posts を初期化
  const [posts, setPosts] = useState([]);
  
  //外部データを取得する副作用関数を返す
  useEffect(() => {
    //副作用の実行を確認するための出力(テスト用)
    console.log('レンダーしました。');
    
    //fetch() メソッドで外部データを取得
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => {
        //json() メソッドでレスポンスボディを取得してJSONとして返す
        return response.json();
      })
      .then((json) => {
        //引数で受け取った JSON を posts に更新
        setPosts(json)
      })
  },[setPosts]) //依存変数に state の更新関数 setPosts を指定
  
  // map() メソッドで取得したデータを展開
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

ReactDOM.render(
  <MyPosts />,
  document.querySelector("#root")
);

useEffect の第2引数に適切な依存変数を指定しないと、以下のようにフェッチが継続してしまいます。

第2引数がない場合、副作用は毎回のレンダー時に走り、内部で state をセットしてると再度副作用をトリガーするため無限ループになります。また、依存関係を表す第2引数に常に変わる値(この例の場合では posts)が入ってる場合でも無限ループが起きます。

以下はレスポンスの取得に、Async Function の async/await を使う例です。

function MyPosts() {

  const [posts, setPosts] = useState([]);

  // コールバック内で async 関数を定義して実行
  useEffect(() => {
    // async 関数を定義
    async function fetchData() {
      // Response オブジェクトを取得
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts"
      );
      const json = await response.json();
      setPosts(json);
    }
    
    //上記で定義した async 関数を実行して返す
    fetchData();
  }, [setPosts]); 
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

または、以下のように即時関数として実行して返すこともできます。

function MyPosts() {

  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // 即時関数として実行
    (async function() {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts"
      );
      const json = await response.json();
      setPosts(json);
    })();
  }, [setPosts]); 
  
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

サスペンスを使ったデータ取得(実験的機能)

まだ、現時点では実験的機能となっていますが、新しいデータ取得の機能として Suspense があります。

クリーンアップ関数

副作用関数の中でイベントリスナの登録やタイマーのセット、非同期通信の subscribe などをしている場合は、メモリリークが発生しないようにクリーンアップが必要になります。

例えば、イベントリスナを登録している場合、クリーンアップを行わないとコンポーネントがマウントされるたびにイベントが重複して登録されてしまいますし、タイマーのセットをしている場合、クリーンアップを行わないと多重にタイマーが実行されてしまいます。

useEffect に渡す副作用関数ではクリーンアップを行うためのクリーンアップ関数を返すことができます。

副作用関数でクリーンアップ関数を返すと、コンポーネントが UI から削除される前にクリーンアップ関数が呼び出されます。コンポーネントが複数回レンダーされる場合は、新しい副作用を実行する前に前回の副作用はクリーンアップされます。

以下はクリーンアップ関数が呼び出されるタイミングを確認する例です。

App コンポーネントには2つのボタンがあり、1つ目のボタンをクリックすると state 変数 random が新しい値に変更されるのでコンポーネントが再レンダリングされます。

2つ目のボタンはトグルボタンになっていて、クリックすると state 変数 mounted の真偽値がトグルして、MyEffect コンポーネントをアンマウント(false)/マウント(true)します(40行目)。

useEffect の副作用関数はコンソールに「useEffect を実行」と出力するだけのものですが、クリーンアップ関数として「クリーンアップを実行」とコンソールに出力する関数を返しています。

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function MyEffect() {

  useEffect(() => {
    // 毎レンダー後に呼び出されてコンソールに出力
    console.log('useEffect を実行');
    // アンマウントされる前に呼び出されて出力される(クリーンアップ関数)
    return () => console.log('クリーンアップを実行');
  })

  return <h2>MyEffect</h2>
}

function App() {
  // 値を更新することで再レンダリングさせるためだけの state 変数 random
  // 乱数を値に設定(生成した random 自体は使わない)
  const [random, setRandom] = useState(Math.random());

  // MyEffect の状態を表す state 変数 mounted
  // MyEffect が表示(マウント)されてれば true 、非表示(アンマウント)の場合は false
  const [mounted, setMounted] = useState(true);

  //「再レンダー」ボタンのイベントハンドラ(新しい乱数を生成)
  const reRender = () => setRandom(Math.random());

  // mounted の値をトグルさせ、MyEffect をアンマウント/マウントさせるイベントハンドラ
  const toggleMount = () => setMounted(!mounted);

  return (
    <>
      <button onClick={reRender}>
        再レンダー
      </button>
      <button onClick={toggleMount}>
        {mounted ? "アンマウント": "マウント"} 
      </button>
      {mounted && <MyEffect/>}
    </>
  );
}

ReactDOM.render(
  <App />,
  document.querySelector("#root")
);

初期状態では、state 変数 mounted は true なので MyEffect コンポーネントはマウントされ、useEffect の副作用関数により「useEffect を実行」と表示されます。

「アンマウント」というボタンをクリックすると、イベントハンドラにより mounted が false になり MyEffect コンポーネントがアンマウントされて、クリーンアップ関数により「クリーンアップを実行」と表示されます。

この状態で「再レンダー」をクリックすると App コンポーネントは再レンダリングされますが、MyEffect コンポーネントはアンマウントされているのでコンソールには何も出力されません。

「マウント」をクリックすると、mounted が true になり MyEffect コンポーネントがマウントされ、useEffect の副作用関数により「useEffect を実行」と表示されます。

「再レンダー」をクリックすると、random が更新されて、コンポーネントが再レンダリングされ、useEffect の副作用が実行されます。

その際、新しい副作用を実行する前にクリーンアップ関数により前回の副作用がクリーンアップされ「クリーンアップを実行」と表示され、その後副作用が実行され「useEffect を実行」と表示されます。

※ useEffect から返すクリーンアップ関数は、コンポーネントがアンマウントされたときにのみ呼び出されるわけではなく、副作用が実行される前に毎回呼び出され、最後(前)の副作用の実行をクリーンアップします。

タイマーの例

以下はタイマーを使って毎秒1づつ増加していくカウンターを表示するコンポーネントの例です。

アンマウントされる際に、クリーンアップ関数で clearInterval を使ってタイマーをクリアしています。

count を更新する関数 setCount では、関数型の更新を使っています。これにより、新しい count が前の count に基づいて算出されるので、count を依存変数に指定する必要はなく、空の配列 [ ] を指定することで副作用を1度だけ実行します。

以下の例では副作用やクリーンアップ関数がどのようなタイミングで実行されるかを確認するためにコンソールに出力する記述を追加しています。また、アンマウントする際の動作を確認するために、カウンターのコンポーネントをアンマウント・マウントするボタンを親コンポーネントに設置してあります。

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

//毎秒1づつ増加していくカウンターを表示するコンポーネント
function MyCounter() {

  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('useEffect を実行')
    
    const id = setInterval(() => {
      setCount(count => count + 1);
      console.log('count up');
    }, 1000)
    
    //クリーンアップ関数
    return () => {
      clearInterval(id)
      console.log('クリーンアップを実行')
    }
  },[]) //空の配列を指定(副作用とそのクリーンアップを1度だけ実行)

  return <h2>Count: {count}</h2>;
}

function App() {
  const [mounted, setMounted] = useState(true);
  const toggleMount = () => setMounted(!mounted);

  return (
    <>
      <button onClick={toggleMount}>
        {mounted ? "アンマウント": "マウント"} 
      </button>
      {mounted && <MyCounter />}
    </>
  );
}

ReactDOM.render(
  <App />,
  document.querySelector("#root")
);

初回のレンダリング後、副作用が一度だけ実行されて「useEffect を実行」とコンソールに出力され、カウントアップされていき、その都度「count up」とコンソールに出力されます。

「アンマウント」とあるボタンをクリックすると、MyCounter コンポーネントがアンマウントされ、クリーンアップ関数が呼び出され、clearInterval が実行されて、その後「クリーンアップを実行」とコンソールに出力されます。

以下のように count を更新する関数 setCount を書き換えて、依存変数に count を指定しても機能しますが、count が変わる度に clearInterval が実行されてしまうので、この場合はあまり望ましくありません。

この場合、count の値が変われば副作用は再実行されて、 setCount(count + 1) はそのレンダーに定義されている count を参照します。

//機能するが count が変わる度に interval が clear される例
function MyCounter() {

  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('useEffect を実行')
    
    const id = setInterval(() => {
     //関数型の更新ではなく新しい count の値を指定
      setCount(count + 1);
      console.log('count up');
    }, 1000)
    
    //クリーンアップ関数
    return () => {
      clearInterval(id)
      console.log('クリーンアップを実行')
    }
  },[count])  //依存変数に count を指定

  return <h2>Count: {count}</h2>;
}

count の値が変わる度に副作用が再実行され、クリーンアップによりその度に clearInterval が実行されてしまいます。

参考サイト:useEffect完全ガイド

以下はクリーンアップ関数でタイマーをクリアしない悪い例です。

//クリーンアップ関数でタイマーをクリアしない例(悪い例)
function MyCounter() {

  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('useEffect を実行')
    
    setInterval(() => {
      setCount(count => count + 1);
      console.log('count up');
    }, 1000)
    
    //タイマーをクリアしていない(危険)
    return () => {
      //アンマウントのタイミングを出力するためのもの
      console.log('アンマウントされました');
    }

  },[])

  return <h2>Count: {count}</h2>;
}

コンポーネントをアンマウント後もカウントアップが続いていて警告が表示されています。

警告は「マウントされていないコンポーネントで React の state の更新を実行できません。これはアプリケーションのメモリリークを示しています。修正するには、useEffect クリーンアップ関数ですべてのサブスクリプションと非同期タスクをキャンセルします」というような意味です。

イベントの登録の例

コンポーネントがマウントされる度にイベントが重複して登録されないように、クリーンアップ関数で登録を解除します。

useEffect(() => {
  // イベントハンドラ
  function resizeHandler() {
    // 何らかの処理(この例では width をコンソールに出力するだけ)
    console.log(window.innerWidth);
  }

  // イベント(リスナ)の登録
  window.addEventListener('resize', resizeHandler);

  //イベント(リスナ)の解除(クリーンアップ関数)
  return () => {
    window.removeEventListener('resize', resizeHandler);
  };

}, []); //空の配列を指定(副作用とそのクリーンアップを1度だけ実行)

参考サイト

useContext

useContext は Context API をより簡単に使えるようなるフックです。

useContext(MyContext) は <MyContext.Consumer>、またはクラスにおける static contextType = MyContext と同等のものと考えることができます。

React では、配下のコンポーネントにデータを渡すための手段として props が提供されていますが、props の場合、親から子へデータをバケツリレーのように渡していかなければなりません。

例えば RootComponent を一番上の親コンポーネントとして、その子コンポーネントに ComponentA、その子コンポーネントに ComponentB、その子コンポーネントに ComponentC を持つ4階層のコンポーネントがある場合、RootComponent の props を ComponentC に渡すには ComponentA 及び ComponentB 経由で ComponentC まで伝播させなければなりません。

Context API を使うとバケツリレーを行わずにデータを RootComponent から ComponentC に渡すことができます。

以下は useContext を使わない従来の Context API の使用例です。

import React from 'react';
import ReactDOM from 'react-dom';
 
//コンポーネントの外で Context オブジェクトを作成
const ColorContext = React.createContext();
 
const RootComponent = (props) => {
  //Provider コンポーネントで適用範囲を囲み value プロパティに渡したい値を設定
  return (
    <div>
      <h1>Root Component</h1>
      <ColorContext.Provider value={props.color}>
        <ComponentA />
      </ColorContext.Provider>
    </div>
  )
}
 
const ComponentA = (props) => {
  //props の受け渡しはない
  return (
    <div>
      <h3>Componet A</h3>
      <ComponentB />
    </div>
  )
}
 
const ComponentB = (props) => {
  //props の受け渡しはない
  return (
    <div>
      <h4>Componet B</h4>
      <ComponentC />
    </div>
  )
}
 
const ComponentC = (props) => {
  //Consumer コンポーネントで値を受け取る
  return (
    <ColorContext.Consumer>
      {(value) => (
        <div>
          <h5>Componet C | color : {value}</h5>
        </div>
      )}
    </ColorContext.Consumer>
  )
}
 
ReactDOM.render(
  <RootComponent color="green"/>,
  document.getElementById('root')
)

通常はコンポーネントを個別のファイルに記述し、Context オブジェクトを export して受け取るコンポーネントで import しますが、この例では全てのコンポーネントを1つのファイルで記述しています。

以下 は useContext を使って上記を書き換えた例で、データの受取がとても簡潔に記述できます。

変更は、冒頭の useContext のインポートの記述の追加とデータを受け取る ComponentC の部分です。ComponentC では単に useContext の記述だけでデータを受け取ることができます。

//useContext を react 本体から(名前付き)インポート
import React, { useContext } from 'react';
import ReactDOM from 'react-dom';
 
//コンポーネントの外で Context オブジェクトを作成(前の例と同じ)
const ColorContext = React.createContext();
 
const RootComponent = (props) => { //前の例と同じ
  //Provider コンポーネントで適用範囲を囲み value プロパティに渡したい値を設定
  return (
    <div>
      <h1>Root Component</h1>
      <ColorContext.Provider value={props.color}>
        <ComponentA />
      </ColorContext.Provider>
    </div>
  )
}
const ComponentA = (props) => { //前の例と同じ(省略)}
const ComponentB = (props) => { //前の例と同じ(省略)}

const ComponentC = (props) => {
  // useContext で値を受け取るだけ
  const value = useContext(ColorContext);
  return (
    <div>
      <h5>Componet C | color : {value}</h5>
    </div>
  )
}

ReactDOM.render(
  <RootComponent color="green"/>,
  document.getElementById('root')
)

基本的な使い方

コンテクストを利用するには、コンポーネントの外で React.createContext() を使ってコンテクストオブジェクト(Context)を作成します。

const MyContext = React.createContext();

作成したコンテクストオブジェクトには、関連付けされた Provider コンポーネントが付属しています。渡したい値を Provider コンポーネントの value プロパティに設定すると、その値を子孫コンポーネントで useContext を使って受け取ることができます。

以下は、作成した MyContext の Provider コンポーネント MyContext.Provider の value プロパティに、渡したい値 {props.xxxx} を設定しています。

そして ComponentX を囲んで Context の適用範囲をコンポーネントツリーにおける ComponentX 以下に設定しています。

const App = (props) => { 
  //Provider コンポーネントで適用範囲を囲み value プロパティに渡したい値を設定
  return (
    <div>
      <MyContext.Provider value={props.xxxx}>
        <ComponentX />
      </MyContext.Provider>
    </div>
  )
}

値を受け取るコンポーネント

Provider コンポーネントの value プロパティにセットした値は、useContext を使って受け取ることができます。useContext はコンテクストオブジェクト(React.createContext からの戻り値)を受け取り、そのコンテクストの現在値を返します。

const value = useContext(MyContext);

適用範囲(上記の場合、コンポーネントツリーにおける ComponentX 以下)の任意のコンポーネントでコンテクストの現在値を useContext を使って受け取ることができます。

//適用範囲の任意のコンポーネント
const ComponentY = (props) => {
  // useContext で値を受け取るだけ
  const value = useContext(ColorContext);
  // コンテクストの現在値を参照
  return <div>{value}</div>;
}

useState と使う

単なる値だけではなく useContext を利用して useState の値と更新関数もコンポーネント間で共有することができます。

コンポーネントツリーのどこか深くネストされたコンポーネントからコンテクストを更新する必要がある場合、コンテクストを通して下に関数を渡すことで、ネストされたコンポーネントからコンテクストを更新することができます。

以下の例では theme-context.js で themes というスタイルを表すオブジェクトと ThemeContext という名前のコンテキストを定義してエクスポートし、App コンポーネントと ThemedButton コンポーネントでインポートします。

src/theme-context.js
import React from 'react';

export const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext();

export default ThemeContext;

App.js では useState を import して、useState で設定した theme とその更新関数 setTheme を value に設定して共有できるようにします。theme の初期値は themes.light に設定しています。

ThemeContext.Provider で Toolbar コンポーネントを囲み、適用範囲をその配下としています。

src/App.js
import React, {useState} from 'react';
import ThemeContext, { themes } from './theme-context'
import Toolbar from './Toolbar';

const App = () => {
  
  const [theme, setTheme] = useState(themes.light)
  
  return (
    <ThemeContext.Provider value={[theme,setTheme]}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

export default App;

以下は Toolbar コンポーネントです。子コンポーネントの ThemedButton をインポートして表示します。

src/Toolbar.js
import React from 'react';
import ThemedButton from './ThemedButton';

const Toolbar = () => {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

export default Toolbar;

以下の ThemedButton コンポーネントでは useContext を使って、theme とその更新関数 setTheme を受け取ります。

ボタンの onClick イベントのハンドラに toggleTheme を設定し、toggleTheme では受け取った setTheme を使ってコンテクストを更新します。

useState の更新関数 setTheme では、前回の state の値により、新しい state を更新するので関数型の更新を使って更新された値を返します。

src/ThemedButton.js
import React, { useContext } from 'react';
import ThemeContext, { themes }  from './theme-context'

const ThemedButton = () => {
  const [theme, setTheme] = useContext(ThemeContext);
  
  const toggleTheme = () => {
    setTheme(prevTheme => {
      return  prevTheme === themes.dark ? themes.light : themes.dark;
    });
  };
  
  return (
    <button 
      onClick={toggleTheme} 
      style={{ background: theme.background, color: theme.foreground }}>
      Toggle Theme
    </button>
  );
}

export default ThemedButton;

以下は表示用の src/index.js です。

src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <App />,
  document.getElementById('root')
)

useReducer

useReducer は useState と同様に状態(state)を管理することができるフックです。

useState は単一の state を管理するのに適していますが、useReducer は複数の state を管理する(複数の値にまたがる複雑な state ロジックがある)場合や前の state に基づいて次の state を決める必要がある場合に適しています。

useReducer を使うには useState と同様、 useReducer を react 本体から(名前付き)インポートする必要があります。

//React に加え useReducer をインポート
import React, { useReducer } from 'react';

useReducer はコンポーネントのトップレベルの位置で以下のような構文で呼び出します。

reducer 関数と state の初期値(initialState)を渡すと、state の現在の値(state)と dispatch メソッドをペアにして返します。

const [state, dispatch] = useReducer(reducer, initialState)

reducer は state を更新するための関数です。現在の state と action を引数に受け取り、action の値によって分岐して新しい state の値を返す処理を定義します。

// reducer 関数(現在の state の値と action を引数に受け取る)
const reducer = (state, action) => {
  // action を設定しその値により state の更新方法を定義
}

dispatch メソッドは reducer 関数を実行して値を更新するための呼び出し関数です。

値を更新するには dispatch メソッドを呼び出し、引数に reducer 関数で設定した action を指定して reducer を実行します。

おそらく簡単な例を見たほうがわかりやすいかと思います。

以下は、ボタンをクリックしてカウントの値を増減させるコンポーネント Counter の例です。

// useReducer をインポート
import React, { useReducer } from 'react';
import ReactDOM from 'react-dom';

// reducer 関数の定義
// 現在の state を受け取り、任意の action を設定して state の更新方法を定義
function reducer(state, action) {
  // 設定した action の値によって新しい state の値を返す
  if(action === 'increment'){
    // action が increment を設定して、その場合の値を返す
    return state + 1;
  }else if(action === 'decrement'){
    return state - 1;
  }else {
    throw new Error();
  }
}

function Counter() {
  // useReducer をトップレベルの位置で宣言(フックの呼び出し)
  // 初期値は 0 を設定
  const [state, dispatch] = useReducer(reducer, 0);
  // dispatch の引数には reducer で設定した action を指定して state の値を更新
  return (
    <>
      Count: {state} <br />
      <button onClick={() => dispatch('decrement')}>-</button>
      <button onClick={() => dispatch('increment')}>+</button>
    </>
  );
}

ReactDOM.render(
  <Counter />,
  document.querySelector("#root")
);

上記の例では action と state は文字列の値を設定しています。

action や state はオブジェクトである必要はなく、数値、配列等どんな値でも設定することが可能ですが、複雑な条件に対応できるように state や action はオブジェクトで指定することが多いです。

以下は上記の action と state をオブジェクトで書き換えた例です。

冒頭では state の初期値(initialState)として count を0にを設定しています。コンポーネント内では、count の値は state.count でアクセス(取得)することができます。

続いて reducer 関数を定義しています。reducer 関数は現在の state と action を引数に受け取ります。任意の action を設定して action の値によって新しい state の値を返します。以下の例では action.type として increment と decrement を設定して、それぞれの場合の値を返しています。

この例のように action.type の取りうる値が2つの場合は if 文でも記述できますが、この例では switch 文を使っています。

useReducer の宣言は Counter コンポーネント内に記述します。そしてボタンのクリックイベントのハンドラに dispatch を指定して、state の値を更新します。

//state の初期値の設定
const initialState = {count: 0};

// reducer 関数の定義
function reducer(state, action) {
  // action.type を設定して、その値により処理(新しい値を返す)
  switch (action.type) {
    // action.type が increment を設定
    case 'increment':
      // action.type が increment の場合は現在の値を1増加
      // 現在の count の値はは state.count で取得
      return {count: state.count + 1};
    case 'decrement':
      // action.type が decrement の場合は現在の値を1減少
      return {count: state.count - 1};
    default:
      // action.type が上記以外の場合はエラーをスロー
      throw new Error();
  }
}

function Counter() {
  // useReducer を宣言(initialState は先頭で設定)
  const [state, dispatch] = useReducer(reducer, initialState);
  // dispatch メソッドの引数 action をオブジェクトで設定
  return (
    <>
      Count: {state.count} <br />
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

初期 state の指定

上記の場合、useReducer の第2引数として初期 state を渡しても同じです。

const [state, dispatch] = useReducer(reducer, {count: 0});

state の値の更新

state の値を更新するには dispatch を使います。dispatch の引数に reducer 関数で設定した action を指定すると、設定した action により state の値が更新されます。

この例の場合は、ボタンをクリックした際のイベントハンドラに、対応する action.type を指定した dispatch を設定しています。

action

reducer 関数内の action の分岐を増やすことで action の設定(登録)を増やすことができます。

以下は前述の例に reset ボタンを追加する例です。

const initialState = {count: 0};
 
// reducer 関数の定義
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    // reset の分岐を追加(action.type が reset を設定)
    case 'reset':
      // action.type が reset の場合は現在の値を0に
      return {count: 0};
    default:
      throw new Error();
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  // reset ボタンを追加
  return (
    <>
      Count: {state.count} <br />
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'reset'})}>reset</button>
    </>
  );
}

また、action はオブジェクトなので(オブジェクト以外も設定可能)、任意のプロパティを設定することができます。

以下は payload というプロパティを追加して、reducer 関数の外側で増減する値を変更できるようにする例です。

const initialState = {count: 0};
 
// reducer 関数の定義
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      // action.payload を設定してその値を使って更新
      return {count: state.count + action.payload};
    case 'decrement':
      // action.payload を設定してその値を使って更新
      return {count: state.count - action.payload};
    case 'reset':
      return {count: 0};
    default:
      throw new Error();
  }
}
 
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  // dispatch に payload プロパティを指定して増減する値を10に
  return (
    <>
      Count: {state.count} <br />
      <button onClick={() => dispatch({type:'decrement',payload:10})}>-</button>
      <button onClick={() => dispatch({type:'increment',payload:10})}>+</button>
      <button onClick={() => dispatch({type:'reset'})}>reset</button>
    </>
  );
}

以下は props を使って Counter コンポーネントに増減する値を渡す例です。reducer 関数は前述と同じです。

//props として増減する値(step)を受け取る
function Counter({step}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  // payload プロパティに props の step を設定
  return (
    <>
      Count: {state.count} <br />
      <button onClick={() => dispatch({type:'decrement',payload:step})}>-</button>
      <button onClick={() => dispatch({type:'increment',payload:step})}>+</button>
      <button onClick={() => dispatch({type:'reset'})}>reset</button>
    </>
  );
}
 
//増減する値(step)を渡す
ReactDOM.render(
  <Counter step={5}/>,
  document.querySelector("#root")
);

遅延初期化

初期 state の作成を遅延させるには init 関数を第3引数として渡します。初期 state が init(initialArg) に設定されます。

以下は初期値(initialCount)を props 経由で受け取る例です。

//init 関数の定義
function init(initialCount) {
  return {count: initialCount};
}
 
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      //init 関数を使って更新(リセット)
      return init(action.payload);
    default:
      throw new Error();
  }
}
 
function Counter({initialCount}) {
  //フックの呼び出しで、第3引数に init 関数を渡す
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  // payload プロパティに props の initialCount を設定
  return (
    <>
      Count: {state.count} <br />
      <button onClick={() => dispatch({type:'decrement'})}>-</button>
      <button onClick={() => dispatch({type:'increment'})}>+</button>
      <button onClick={() => dispatch({type:'reset',payload:initialCount})}>reset</button>
    </>
  );
}
 
//初期値(initialCount)を props 経由で渡す
ReactDOM.render(
  <Counter initialCount={100} />,
  document.querySelector("#root")
);