2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React 再入門 (React hook API)

Last updated at Posted at 2023-11-10

React ドキュメント
React hook API は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使える ようになります。React は 関数型 になりつつあるということです。この記事は上記ドキュメントを1ページにまとめ、全体を俯瞰できるようにしたものです。オリジナルサイトにはより基本的で網羅的で詳細な説明があります。

Promise の基礎知識 - Qiita

1.React の基礎

クイックスタート
React はJavaScriptのライブラリで、UI を構築するために使われます。Reactの開発環境を構築するには、create-react-app というツールを使うのが一般的です。コマンド一つでReactアプリのプロジェクトを作成できます。create-react-appを使うには、Node.jsnpm がインストールされている必要があります。

React プロジェクトの始め方
次のコマンドで作成します。

npx create-react-app my-app
cd my-app
npm start

以下のようなソースコードが生成されます。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
reportWebVitals();
src/App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

1-1.コンポーネント

React のコンポーネントとは以下の2つを満たす JavaScript 関数です。

  • 名前は常に大文字で始まる。
  • JSX マークアップを return する。

レンダー とは、React がコンポーネントを呼び出すことです。
コンポーネントがレンダーされるトリガーは2つあります。

  • 1.コンポーネントの初回レンダー。
  • 2.コンポーネント(またはその祖先のいずれか)の state の更新。

レンダーの次のステップは DOM への コミット になりますが、以下のような動作になります。

初回レンダー時 には DOM ノードを作成し、コミットでそのすべての DOM ノードを画面に表示します。
再レンダー時 には、React は前回のレンダーからどの部分が変わったのか、あるいは変わらなかったのかを計算します。コミットでReact は最新のレンダー出力に合わせて DOM を変更するため、必要な最小限の操作(レンダー中に計算されたもの!)を適用します。React はレンダー間で違いがあった場合にのみ DOM ノードを変更します。

コンポーネントのインポートとエクスポート
1 つのファイルに多くのコンポーネントを宣言することもできますが、大きなファイルは取り回しが難しくなります。これを解決するために、コンポーネントを個別のファイルで宣言しエクスポートして、別のファイルからそのコンポーネントをインポートすることができます。

1-2.JSX

React コンポーネント は、ブラウザにレンダーされるマークアップを含んだ JavaScript 関数 です。React コンポーネントは、マークアップを表現するために JSX という JavaScriptの拡張構文 を使用します。JSXHTML によく似ていますが、少し構文が厳密であり、動的な情報を表示することができます。

JSXの基本事項として以下の3点を挙げます。

  • JSX に 波括弧 { } で JavaScript を含める
  • コンポーネントに props を渡す
  • リスト のレンダー

JSX に波括弧で JavaScript を含める
JSX を使うことで、JavaScript ファイル内に HTML のようなマークアップを記述し、レンダーの ロジックコンテンツ を同じ場所に配置することができます。時には、そのマークアップ内でちょっとした JavaScript ロジックを追加したり、動的なプロパティを参照したりしたいことがあります。このような状況では、JSX 内で波括弧を使い JavaScript への「窓を開ける」ことができます。

App.js
const person = {
  name: 'Gregorio Y. Zara',
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

export default function TodoList() {
  return (
    <div style={person.theme}>
      <h1>{person.name}'s Todos</h1>
      <img
        className="avatar"
        src="https://i.imgur.com/7vQD0fPs.jpg"
        alt="Gregorio Y. Zara"
      />
      <ul>
        <li>Improve the videophone</li>
        <li>Prepare aeronautics lectures</li>
        <li>Work on the alcohol-fuelled engine</li>
      </ul>
    </div>
  );
}```

コンポーネントに props を渡す
React コンポーネントでは、props を使ってお互いに情報をやり取りします。親コンポーネントは、子コンポーネントに props を与えることで、情報を渡すことができます。 HTML の属性 (attribute) と似ていますが、 オブジェクト、配列、関数、そして JSX まで、どのような JavaScript の値でも渡すことができます!

App.js
import { getImageUrl } from './utils.js'

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}
  • children プロパティ
    このように JSX タグ内でコンテンツをネストした場合、親側のコンポーネントはその中身を children という props として受け取ります。 例えば上の Card コンポーネントは、<Avatar /> がセットされた children プロパティ を受け取って、ラッパ div 要素の内部にそれをレンダーしています。
utils.js
export function getImageUrl(person, size = 's') {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    size +
    '.jpg'
  );
}

リストのレンダー
データの集まりから複数のよく似たコンポーネントを表示したいことがよくあります。React で JavaScript の filter()map() を使って、データの配列を コンポーネントの配列 に変換することができます。
配列内の各要素には、key を指定する必要があります。 通常、データベースの ID を key として使うことになるでしょう。key は、リストが変更されても各アイテムのリスト内の位置を React が追跡できるようにするために必要です。

App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ul>{listItems}</ul>
    </article>
  );
}
utile.js
export function getImageUrl(person) {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    's.jpg'
  );
}
data.js
export const people = [{
  id: 0,
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1,
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2,
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3,
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4,
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];

key をどこから得るのか
データソースの種類によって key を得る方法は異なります。

  • データベースからのデータ: データがデータベースから来る場合、データベースのキーや ID は必然的に一意ですので、それを利用できます。
  • ローカルで生成されたデータ: データがローカルで生成されて保持される場合(例:ノートを取るアプリにおけるノート)は、アイテムを作成する際に、インクリメンタルなカウンタや crypto.randomUUID()、または uuid などのパッケージを使用します。

1-3.コンポーネントは純粋関数です

コンピュータサイエンス(特に関数型プログラミングの世界)では、純関数 (pure function) とは以下のような特徴を持つ関数のことを指します。

  • 呼び出される前に存在していたオブジェクトや変数を変更しない。副作用を起こさない。
  • 同じ入力 (props , state) には同じ出力。同じ入力を与えると、純関数は常に同じ結果 (JSX) を返す。

これを守らなかった場合、コードベースが複雑になるにつれて、ややこしいバグや予測不能な挙動に遭遇することになります。“Strict Mode” で開発している場合、最初のマウント時に、React は各コンポーネント関数を 2 回呼び出すことで、純粋でない関数が引き起こす間違いに気づきやすくしてくれます。

Haskell は純粋な関数型プログラミングであり、プログラムコードを数学における数式と同じような手法で取り扱うことを可能としています。副作用はプログラミングにおける根幹ですが、Haskellではモナドという抽象的な枠組みでこれに対処しています。これの問題点は難解であるということです。Reactではこの難解さを避けつつ、純粋関数型プログラミングの果実(バグの少なさ、デバッグの容易さ)の獲得を目指しているように見えます。

1-4.副作用を書く場所

コンポーネント関数は純粋ですので、レンダー中は副作用を起こしてはいけません。 その代わりに副作用を書く場所は以下の2カ所に留めるようにします。

  • イベントハンドラ
  • エフェクト

React では、副作用は通常、イベントハンドラ の中に記述します。イベントハンドラは、ボタンがクリックされたといった何らかのアクションが実行されたときに React が実行する関数です。イベントハンドラは、コンポーネントの「内側」で定義されているものではありますが、レンダーの「最中」に実行されるわけではありません! つまり、イベントハンドラは純粋である必要はありません。

エフェクト は、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのものです。後述します。

React におけるイベントハンドラの設定の仕方

  • 引数無し
    onClick={handleClick}      // カッコ無しで関数名のみ
    onClick={() => handleClick()}  //アロー関数
  • 引数有り
    onClick={() => handleClick(arg)}

とりあえずアロー関数にしておけば良い。
onClick={handleClick()} (カッコ有)はレンダー時に関数を呼び出す危険性があるので避けてください。

イベントハンドラとエフェクトでの非同期関数の書き方

以下はイベントハンドラとエフェクトの副作用の例です。

Promise を使った非同期処理の書き方です。fetch は Promise を返します。

イベントハンドラ(Promise)
import React, { useState } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);

  function handleClick() {
    // fetch APIでデータを取得するPromiseを作成
    const promise = fetch('https://example.com/data.json');
    // Promiseが解決されたら、thenメソッドでデータをセットする
    promise.then(response => response.json())
           .then(data => setData(data));
  }

  return (
    <div>
      <button onClick={handleClick}>データを取得</button>
      {data && <p>データ: {data}</p>}
    </div>
  );
}

async/await を使った非同期処理の書き方です。
async を関数の頭につけると Promise を返します。awaitPromise 処理の前において、その処理が終わるまで待つようにします。

イベントハンドラ(async/await)
function MyComponent() {
  const handleClick = async () => {
    // fetch APIでデータを取得するPromise処理の終了を待つ
    const response = await fetch('https://example.com/data');
    const data = await response.json();
    console.log(data);
  };

  return (
    <button onClick={handleClick}>データを取得</button>
  );
}
エフェクト(async/await)(説明は後述)
import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://example.com/data');
      const data = await response.json();
      console.log(data);
    };

    fetchData();
  }, []); // 空の依存配列を指定して、コンポーネントのマウント時にのみ実行

  return <div>データを表示</div>;
}

1-5.Strict Modeでは

Strict Mode は React の 開発モード でのみ有効な機能で、アプリケーションの潜在的な問題点を洗い出すためのツールです。Strict Mode が有効なとき、React は安全でないライフサイクルや副作用を検出するために、コンポーネントを意図的に二重レンダリングします。 これは React 18 から導入された新しいチェックで、コンポーネントが初めてマウントされるたびに、すべてのコンポーネントを自動的にアンマウント・再マウントし、かつ 2 回目のマウントで以前の state を復元します。 これにより、将来的に React が state を保ったままで UI の一部分を追加・削除できるような機能(オフスクリーンなど)を導入したときに、コンポーネントが正しく動作することを確認できます。
これによって エフェクト においては、 クリーンアップ がうまく実装されているかどうかチェックしやすくなります。(エフェクト参照)

Strict Mode は本番環境では影響を与えないため、ユーザが使うアプリを遅くすることはありません。Strict Mode を有効にするには、ルートコンポーネントを <React.StrictMode> でラップします。create react appコマンドではデフォルトで以下のようなファイルが作成されます。

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

1-6.React hook API

React フック (hook API) とは、関数コンポーネント内の再利用可能な部分を切り離して使用できるシンプルな JavaScript 関数 です。フックは状態を持つことができ、副作用を管理することもできます。

フックは JavaScript 関数ですが、利用するときは以下の 2 つのルールに従う必要があります。

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

標準組み込みフック例

  • useState: 状態を管理するためのフックです。状態の値と、それを更新するための更新関数を返します。
  • useReducer: 複雑な状態管理をサポートするための、useState の代替となるフックです。
  • useContext: コンテキストの現在の値を返します。
  • useRef: .current プロパティを持つ ref オブジェクトを返します。そのような ref オブジェクトはミュータブル (変更可能) です。これは主に子コンポーネントに直接アクセスするために使用されます。
  • useEffect: API 呼び出し、サブスクリプション、タイマー、ミューテイションなどのような副作用を管理するためのフックです。
  • useEffectEvent: (実験的な API)エフェクトから非リアクティブなロジックを抽出する。
  • useMemo: 重たい計算を キャッシュ する。

2.[useState] state:コンポーネントのメモリ

state の管理

コンポーネントによっては、ユーザ操作の結果として画面上の表示内容を変更する必要があります。フォーム上でタイプすると入力欄が更新される、画像カルーセルで「次」をクリックすると表示される画像が変わる、「購入」をクリックすると買い物かごに商品が入る、といったものです。コンポーネントは、現在の入力値、現在の画像、ショッピングカートの状態といったものを「覚えておく」必要があります。React では、このようなコンポーネント固有のメモリのことを state と呼びます。

以下はコンポーネントがレンダーされる際に、firstName と lastName という2つの state 変数から、 fullName を計算しています。

App.js
import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

2-1.state の保持とリセット

コンポーネントを再レンダーする際、React はツリーのどの部分を保持(および更新)し、どの部分を破棄または最初から再作成するかを決定する必要があります。大抵は React の自動的な挙動でうまくいきます。デフォルトでは、React は以前にレンダーされたコンポーネントツリーと「一致する」部分のツリーを保持します。同じ位置の同じコンポーネントは state が保持されます。

ただし、場合によってはこれが望ましくなく、明示的に state をリセットする必要がある場合があります。state をリセットする方法 は、2 つあります。

  • 1. コンポーネントを異なる位置でレンダーする
  • 2.key を使って各コンポーネントに明示的な識別子を付与する

以下のチャットアプリでは、メッセージを入力した後に送信先を切り替えると、入力フィールドがリセットされるようにしています。これにより、ユーザが誤って間違った相手にメッセージを送信してしまうのを防いでいます。

App.js
import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />  // key
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

例えば <Chat key={email} /> のように異なる key を渡すことでデフォルトの動作を上書きし、コンポーネントの state を強制的にリセットすることができます。 これにより、送信先が異なる場合は異なる Chat コンポーネントであり、新しいデータ(および入力フィールドなど)で最初から再作成する必要があると React に伝えることができます。これにより、全く同じコンポーネントをレンダーしても、送信先を切り替えると入力フィールドがリセットされるようになります。

Chat.js
import { useState } from 'react';

export default function Chat({ contact }) {
  const [text, setText] = useState('');
  return (
    <section className="chat">
      <textarea
        value={text}
        placeholder={'Chat to ' + contact.name}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button>Send to {contact.email}</button>
    </section>
  );
}
ContactList.js
export default function ContactList({
  selectedContact,
  contacts,
  onSelect
}) {
  return (
    <section className="contact-list">
      <ul>
        {contacts.map(contact =>
          <li key={contact.id}>
            <button onClick={() => {
              onSelect(contact);
            }}>
              {contact.name}
            </button>
          </li>
        )}
      </ul>
    </section>
  );
}

2-2.レンダーの state はスナップショット

JSX 内の props、イベントハンドラ、ローカル変数はすべて、レンダー時の state を使用して計算されます。言い換えると、React はレンダー内の state の値を「固定」し、イベントハンドラ内で保持します。 コードが実行されている途中で state が変更されたかどうか心配する必要はありません。
つまり以下のコードで最初にクリックした場合、イベントハンドラの中では、レンダー時のnumber=0 が静的に決められており、0+1 でが3回繰り返され、最終的に number=1 と計算されます。number=3 とはなりません。

App.js
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

2-3.React は state 更新(更新用関数)をまとめて処理する

次にレンダー前に同じ state 変数を複数回更新する場合、setNumber(number + 1) のようにして次の state 値を渡すのではなく、代わりに setNumber(n => n + 1) のようなキュー内のひとつ前の state に基づいて次の state を計算する関数 (更新用関数) を渡すことができます。これは、state の値を単に置き換えるのではなく、代わりに React に「その state の値に対してこのようにせよ」と伝えるための手段です。

App.js
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )
}

ここで、n => n + 1 は 更新用関数 (updater function) と呼ばれます。

  • (1) React はこの関数をキューに入れて、イベントハンドラ内の他のコードがすべて実行された後に処理されるようにします。
  • (2) 次のレンダー中に、useState が呼び出されると、React はこのキューを処理し、最後に更新された state を返します。

n は 0 から始まって、n + 1 を3 回繰り返して、React は 3 を最終結果として保存し、useState から返します。つまりこの例では “+3” をクリックしたときに、値が正しく 3 ずつ増加するのです。

2-4.state を更新するイベントハンドラ

state を使って入力に反応する

React は UI を操作するための宣言的な方法を提供します。UI の個々の部分を直接操作するのではなく、コンポーネントが取りうる異なる状態 (state) を記述し、ユーザの入力に応じてそれらの状態を切り替えます。

ユーザが回答を送信できるフォームを考えてみましょう。

  • フォームに何かを入力すると、“Submit” ボタンが有効になる。
  • “Submit” ボタンを押すと、フォームとボタンが無効になり、スピナが表示される。
  • ネットワークリクエストが成功すると、フォームは非表示になり、お礼メッセージが表示される。
  • ネットワークリクエストに失敗した場合、エラーメッセージが表示され、フォームが再び有効になる
App.js
import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

3.[useReducer] state ロジックをリデューサに集約

state ロジックをリデューサに抽出する

多くのイベントハンドラにまたがって state の更新コードが含まれるコンポーネントは、理解が大変になりがちです。 このような場合、コンポーネントの外部に、リデューサ (reducer) と呼ばれる 単一の関数 を作成し、すべての state 更新ロジックを集約 することができます。

useReducer フック は 2 つの引数を取ります。

  • リデューサ関数
  • 初期 state (tasks)

そして次のものを返します。

  • state 値 (tasks)
  • ディスパッチ関数 (ユーザアクションをリデューサに「ディスパッチ」する)
App.js
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import tasksReducer from './tasksReducer.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

以下の リデューサ関数が、state のロジックを記述する場所です。現在の state とアクションオブジェクトの 2 つを引数に取り、次の state を返すようにします。

tasksReducer.js
export default function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

リデューサ (reducer) の長所を2点挙げます。

  • 可読性: シンプルな state 更新の場合は useState を読むのは非常に簡単です。しかし、より複雑になると、コンポーネントのコードが肥大化し、見通すことが難しくなります。このような場合、useReducer を使うことで、更新ロジックによって書かれる「どう更新するのか」と、イベントハンドラに書かれる「何が起きたのか」とを、きれいに分離することができます。

  • テスト: リデューサはコンポーネントに依存しない 純関数 です。これは、リデューサをエクスポートし、他のものとは別に単体でテストできることを意味します。

4.[useContext] コンテクストで深くデータを受け渡す

コンテクストで深くデータを受け渡す

通常、親コンポーネントから子コンポーネントには props を使って情報を渡します。しかし、props を多数の中間コンポーネントを経由して渡さないといけない場合や、アプリ内の多くのコンポーネントが同じ情報を必要とする場合、props の受け渡しは冗長で不便なものとなり得ます。コンテクスト (Context) を使用することで、親コンポーネントから props を明示的に渡さずとも、それ以下のツリー内の任意のコンポーネントが情報を受け取れるようにできます。

App.js
import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Section コンポーネントは子要素を直接レンダーできますが、これを コンテクストプロバイダ(LevelContext.Provider) でラップし、LevelContext の値を提供します。これにより、「この <Section> の下にあるコンポーネントが LevelContext の値を要求した場合、この level を渡せ」 と React に伝えていることになります。コンポーネントは、UI ツリー内の上側で、最も近い <LevelContext.Provider> の値を使用します。

Section.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);  // LevelContext から値を読み取る
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>  // 下層に新たな値を渡す
        {children}
      </LevelContext.Provider>
    </section>
  );
}
Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);  // 最も近い親の値を使う
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}

複数のコンポーネントから使うことができるように、ファイルを作ってコンテクストをエクスポートする必要があります。

LevelContext.js
import { createContext } from 'react';

export const LevelContext = createContext(0);

5.リデューサとコンテクスト

リデューサとコンテクストでスケールアップ

リデューサ を使えば、コンポーネントの state 更新ロジックを集約する ことができます。コンテクスト を使えば、他のコンポーネントに深く情報を渡す ことができます。そしてリデューサとコンテクストを組み合わせることで、複雑な画面の state 管理ができるようになります。

以下のような TaskApp を考えた場合、 tasks state と dispatch 関数は、props 経由で渡すのではなく、コンテクストに入れる方が望ましい場合があります。こうすることで TaskApp ツリーの下部にある任意のコンポーネントが、“props の穴掘り作業 (prop drilling)” を繰り返さずともタスクのリストを読み取り、アクションをディスパッチすることができるようになります。

以下がリデューサをコンテクストと組み合わせる方法です。

  • 1. コンテクストを作成する。
  • 2. state と dispatch をコンテクストに入れる。
  • 3. ツリー内の任意の場所でコンテクストを使用する。
App.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

【ステップ 1】 tasks と dispatch のコンテクストを作成する。

【ステップ 2】 useReducer() の返り値として tasks と dispatch を取得し、それらを下位のツリー全体に提供する。

TasksContext.js
import { createContext, useContext, useReducer } from 'react';

const TasksContext = createContext(null);

const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer( tasksReducer, initialTasks );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

【ステップ 3】 ツリー内の任意の場所でコンテクストを使う。 以下の関数では useState 関数と useTasksDispatch 関数で使えるようになっている。

AddTask.js
import { useState } from 'react';
import { useTasksDispatch } from './TasksContext.js';

export default function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useTasksDispatch();
  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        }); 
      }}>Add</button>
    </>
  );
}

let nextId = 3;
TaskList.js
import { useState } from 'react';
import { useTasks, useTasksDispatch } from './TasksContext.js';

export default function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <Task task={task} />
        </li>
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();
  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

6.[useRef] ref で値を参照する

ref で値を参照する

コンポーネントに情報を「記憶」させたいが、その情報が新しいレンダーをトリガしないようにしたい場合、ref を使うことができます。
以下のカウンターの例では、カウンターはマークアップに使われておらずレンダーとは無関係なので、state ではなく ref を使っています。

React]App.js
import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );

ノードにフォーカスを当てたり、スクロールさせたり、サイズや位置を測定したりするなどの場合に、React が管理する DOM 要素へのアクセスが必要なことがあります。React にはこれらを行う組み込みの方法が存在しないため、DOM ノードを参照する ref が必要になります。

DOM ノードへの ref の取得
React が管理する DOM ノードにアクセスするには、まず useRef フックをインポートします。

import { useRef } from 'react';

次に、それを使ってコンポーネント内で ref を宣言します。

const myRef = useRef(null);

最後に、参照を得たい DOM ノードに対応する JSX タグの ref 属性にこの ref を渡します。

<div ref={myRef}>

useRef フックは、current という単一のプロパティを持つオブジェクトを返します。最初は myRef.current は null になっています。React がこの <div> に対応する DOM ノードを作成すると、React はこのノードへの参照を myRef.current に入れます。その後、イベントハンドラからこの DOM ノードにアクセスし、ノードに定義されている組み込みのブラウザ API を使用できるようになります。

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

ref はミュータブルです。つまりレンダープロセス外(イベントハンドラ)で current の値を変更・更新できます。逆にレンダー中に current の値を読み取る(または書き込む)べきではありません。
何故なら、ref は変更しても再レンダーがトリガされませんので、レンダー時は current の値は最新値ではないからです。

テキスト入力フィールドにフォーカスを当てる例です。

App.js
import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}
  • 1. useRef フックを使って inputRef を宣言する。
  • 2. それを <input ref={inputRef}> として渡す。これにより、React にこの の DOM ノードを inputRef.current に入れるよう指示している。
  • 3. イベントハンドラ handleClick 関数内で、inputRef.current から入力フィールドの DOM ノードを読み取り、inputRef.current.focus() のようにして focus() を呼び出す。
  • 4. <button> の onClick に handleClick イベントハンドラを渡す。

DOM 操作は ref の最も一般的な使用例ですが、useRef フックはほかに、タイマー ID などの React 外部にあるものを格納するためにも使用できます。state と同様に、ref はレンダー間で維持されます。ref は、セットしても再レンダーがトリガされない state 変数のようなものです。

7.[useEffect] エフェクト

エフェクトを使って同期を行う

エフェクトは、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのものです。

コンポーネントがレンダーされるたびに、React は画面を更新し、その後で useEffect 内のコードを実行します。 言い換えると、useEffect はレンダー結果が画面に反映され終わるまで、コードの実行を「遅らせ」ます。 つまりレンダー自身は純粋を保ちます。

エフェクトは、コミットの最後に、画面が更新された後に実行されます。ここが、React コンポーネントを外部システム(ネットワークやサードパーティのライブラリなど)と同期させるのに適したタイミングです。

7-1.エフェクトの書き方

  • (1) useEffect でコードを書く
  • (2) エフェクトの 依存配列 を指定する
  • (3) 必要に応じて クリーンアップ を追加する

【依存配列】エフェクトはリアクティブ (reactive) な値に “反応” する
以下の例では、エフェクトが 2 つの変数 (serverUrl と roomId) を利用していますが、依存配列には roomId のみが指定されています。

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId]);
  // ...
}

serverUrl が変化することはないので、依存配列に追加しても意味がありません。
一方、roomId は再レンダー時に異なる値になる可能性があります。コンポーネント内で宣言された props、state やこれらの値から導出される値 もまた、レンダー時に計算され、React のデータフローに含まれるため、リアクティブ です。もし serverUrl が state 変数だった場合、リアクティブとなります。

依存配列を「選ぶ」ことはできません。依存配列には、エフェクトで読み取るすべてのリアクティブな値を含める必要があります。リンタがこれを強制します。

7-2.チャットアプリサンプル

以下のコードがフルのチャットアプリサンプルになります。上記の3ステップをコメントで明記してあります。

App.js
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  // (1)useEffectでコードを書く
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect(); // (3)必要に応じてクリーンアップを追加する
  }, [roomId]);   // (2)エフェクトの依存値 (dependency) の配列を指定する
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

useEffect の呼び出しの第 2 引数として依存値の配列を指定することで、React にエフェクトの不必要な再実行をスキップするように指示できます。

useEffect の戻り値としてクリーンアップ関数を指定することができます。React は、エフェクトが再度実行される前に毎回クリーンアップ関数を呼び出し、コンポーネントがアンマウントされる(削除される)ときにも最後の 1 回の呼び出しを行います。
この場合「show=true」=>「show=false」時に ChatRoom コンポーネントがアンマウントされ、クリーンアップ関数の connection.disconnect() が呼ばれます。

chat.js
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

7-3.クリーンアップ

エフェクトが何かをフェッチ(fetch, 取得)する場合、クリーンアップ関数は、フェッチを中止するか、その結果を無視する必要があります。
例えば連続してフェッチするような場合は、最初の結果が得られる前に後続のフェッチが行われ、競合状態に陥る危険性があります。このような競合状態を修正するには、クリーンアップ関数を追加して古いレスポンスを無視する必要があります。

React は、エフェクトが再度実行される前に毎回クリーンアップ関数を呼び出し、コンポーネントがアンマウントされる(削除される)ときにも最後の 1 回の呼び出しを行います。

このような競合に関するバグは、手動での徹底的なテストがないと見逃しやすいものです。これらをすばやく見つけるために、開発環境では React は、初回マウント直後にすべてのコンポーネントを一度だけ再マウントします。

データのフェッチの問題

エフェクトが何かをフェッチ(fetch, 取得)する場合、クリーンアップ関数は、フェッチを中止するか、その結果を無視する必要があります。

useEffect(() => {
  // 関数のローカル変数は useEffect 関数の起動時にスタック領域に確保される
  // useEffectが呼ばれる都度、ignore変数は新しく作られる
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {  // state 更新の競合を回避する
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

エフェクトは通常、コンポーネントを外部システムと同期させるのに使います。 外部システムがなく、他の state に基づいて state を調整したいだけの場合、エフェクトは必要ありません。 また依存配列を適切に指定することによって、不必要なタイミングで再同期(再接続)を行う無駄を避ける必要があります。

同期接続の問題
ChatRoom コンポーネントが、さまざまな画面がある大規模なアプリの一部であると想像してみてください。ユーザは ChatRoom ページからナビゲーションを始めます。コンポーネントがマウントされ、connection.connect() が呼び出されます。次に、ユーザが別の画面、例えば設定ページに移動します。ChatRoom コンポーネントがアンマウントされます。最後に、ユーザが戻るボタンをクリックし、ChatRoom が再びマウントされます。これにより 2 つ目の接続が設定されます…が、最初の接続は破棄されていません! ユーザがアプリ内で移動するたびに、接続がどんどん積み重なっていくことになります。

このようなバグは、手動での徹底的なテストがないと見逃しやすいものです。これらをすばやく見つけるために、開発環境では React は、初回マウント直後にすべてのコンポーネントを一度だけ再マウントします。

この問題を解決するには、エフェクトからクリーンアップ関数を返すようにします。

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []);

依存配列が 無い場合 と 空[]の場合

依存配列がない場合と、空の [] という依存配列がある場合の挙動は異なります。

useEffect(() => {
  // 毎回のレンダー後に実行される
});

useEffect(() => {
  // マウント時(コンポーネント出現時)のみ実行される
}, []);

useEffect(() => {
  // マウント時と、a か b の値が前回のレンダーより変わった場合に実行される
}, [a, b]);

7-4.依存値に関数やオブジェクトを指定しない

JavaScript では、新しく作成されたオブジェクトや関数は、他のすべてのオブジェクトや関数とは異なると見なされます。中身が同じであっても関係ありません!

以下は間違いです。

間違い
function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = { // <=== オブジェクトが常に作り直され、違う値となる
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);  // <== 違う値のオブジェクトで再実行される

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

以下が正しいです。

正しい
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared

7-5.アプリケーション初期化はエフェクトではない

アプリケーションの起動時に一度だけ実行されるべきロジックがあります。そのようなものはコンポーネントの外に置くことができます。トップレベルのコードは、コンポーネントがインポートされたとき(仮にそれが一切レンダーされなかったとしても)に、一度だけ実行されます。 コンポーネントをインポートする際の遅延や予期せぬ動作を避けるため、このパターンは過剰に使用しないでください。アプリ全体の初期化ロジックは、App.js のようなルートコンポーネントモジュールやアプリケーションのエントリーポイントに保持するようにしましょう。

if (typeof window !== 'undefined') { // Check if we're running in the browser.
   // ✅ Only runs once per app load
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

7-6.DOM 操作

React では、レンダーは JSX の純粋な計算であるべきであり、DOM の変更のような副作用を含んではいけません。
それに、レンダー中に VideoPlayer を呼び出せば、最初の呼び出し時には、その DOM はそもそも存在していません! React は JSX が返されるまでどんな DOM を作成したいのか分からないのですから、play() や pause() を呼び出すための DOM ノードはまだ存在していません。

ここでの解決策は、副作用を useEffect でラップして、レンダーの計算処理の外に出すことです。

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

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

DOM の更新をエフェクトでラップすることで、React が先にまず画面を更新できるようになります。その後、エフェクトが実行されます。

VideoPlayer コンポーネントがレンダーされるとき(初回または再レンダーのいずれでも)、いくつかのことが起こります。まず、React は画面を更新し、正しいプロパティを持つ タグが DOM に存在するようにします。次に、React はエフェクトを実行します。最後に、エフェクトは isPlaying の値に応じて play() または pause() を呼び出します。

7-7.useEffectフックによるライフサイクルメソッドの代替

コンポーネントのライフサイクルとは、コンポーネントが生成されてから破棄されるまでの処理の流れのことです。クラスコンポーネントでは、ライフサイクルメソッドと呼ばれる特別なメソッドを使って、ライフサイクルの各段階に処理を追加することができます。

関数コンポーネントでは、useEffectフックを使って、これらのライフサイクルメソッドを代替的に実現することができます。

  • コールバック関数は、副作用を実行する関数です。API通信やDOM操作などの処理を記述します。コールバック関数は、初回レンダリング時と依存配列の値が変更されたときに実行されます。コールバック関数は、クリーンアップ関数を返すことができます。クリーンアップ関数は、コンポーネントがアンマウントされるときや、次のコールバック関数が実行される前に実行されます。イベントリスナーの解除やメモリの開放などの処理を記述します。

  • 依存配列は、コールバック関数の実行タイミングを制御する配列です。依存配列には、コールバック関数が参照する変数やpropsやstateを指定します。依存配列が空の場合、コールバック関数は初回レンダリング時にのみ実行されます。依存配列を省略すると、コールバック関数は毎回のレンダリング時に実行されます。

useEffectフックを使って、クラスコンポーネントのライフサイクルメソッドを模倣する例を以下に示します。

  • componentDidMount:依存配列を空にすると、コールバック関数は初回レンダリング時にのみ実行されます。
useEffect(() => {
  // componentDidMountに相当する処理
}, []);
  • componentDidUpdate:依存配列に変更を検知したい変数やpropsやstateを指定すると、コールバック関数は初回レンダリング時と依存配列の値が変更されたときに実行されます。
useEffect(() => {
  // componentDidUpdateに相当する処理
}, [value]); // valueの変更を検知する
  • componentWillUnmount:コールバック関数がクリーンアップ関数を返すと、その関数はコンポーネントがアンマウントされるときに実行されます。
useEffect(() => {
  // componentDidMountやcomponentDidUpdateに相当する処理
  return () => {
    // componentWillUnmountに相当する処理
  };
}, []);

8.[useEffectEvent] エフェクトイベント(※実験的API)

イベントとエフェクトを切り離す

エフェクトがリアクティブな値を読み取る場合、依存配列としてそれを指定する必要があります。そして、再レンダーによってその値が変更された場合、React は新しい値でエフェクトのロジックを再実行します。コンポーネントの本体部分で宣言された props、state 変数を リアクティブ な値 (reactive value) と呼びます

しかし(リアクティブである)エフェクトの中にあるにもかかわらず、リアクティブであってほしくない場合があります。このような場合、useEffectEvent という特別なフックを使って、エフェクトから非リアクティブなロジックを抽出します。

(例)チャット

ユーザがチャットに接続したときに通知を表示したいとします。正しい色で通知を表示することができるよう、props から現在のテーマ(ダークまたはライト)を読み取ります。

しかし、エフェクト内で使用している限り、theme はリアクティブな値であり(再レンダーの結果として変化する可能性がある)、エフェクトが読み取るすべてのリアクティブな値は、依存値 として宣言する必要があります。

roomId が変わると、期待通りチャットが再接続されます。しかし、theme も依存値であるため、ダークテーマとライトテーマを切り替えることでも毎回チャットが再接続されてしまいます。これは期待外のことです。

つまり、theme は(リアクティブである)エフェクトの中にあるにもかかわらず、リアクティブであってほしくないということです。

こうした場合、 theme という変数に反応させたくないので、非リアクティブなロジックとして抽出し、useEffectEvent の中にまとめます。こうすることで theme を依存配列から外すことが可能になります。

App.js
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}
chat.js
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  let connectedCallback;
  let timeout;
  return {
    connect() {
      timeout = setTimeout(() => {
        if (connectedCallback) {
          connectedCallback();
        }
      }, 100);
    },
    on(event, callback) {
      if (connectedCallback) {
        throw Error('Cannot add the handler twice.');
      }
      if (event !== 'connected') {
        throw Error('Only "connected" event is supported.');
      }
      connectedCallback = callback;
    },
    disconnect() {
      clearTimeout(timeout);
    }
  };
}

9.[useMemo] メモ化

エフェクトは必要ないかもしれない

重たい計算を キャッシュ する(あるいは “メモ化する (memoize)”)には、useMemo フックでラップします。

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ Does not re-run getFilteredTodos() unless todos or filter change
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}

これは、todos または filter のどちらかが変更されない限り、中の関数を再実行しないよう React に指示するものです。 React は初回レンダー時に getFilteredTodos() の返り値を覚えておきます。次回以降のレンダーでは、todos または filter が異なるかどうかをチェックします。前回と同じ場合、useMemo は最後に格納した結果を返します。異なる場合、React は再び中の関数を呼び出し、その結果を格納します。

useMemo でラップする関数はレンダー中に実行されるため、 純粋 (pure) な計算に対してのみ機能します。

10.[useXxxxx] カスタムフック

カスタムフックでロジックを再利用する

Reactのカスタムフックとは、自分で作ったフックのことです。フックとは、Reactの機能の一つで、関数コンポーネントに状態や副作用などを追加できるものです。 カスタムフックを使うと、複数のコンポーネントで共通のロジックを再利用できるようになります。
カスタムフックを作るには、以下のルールに従ってください。

  • カスタムフックの名前は「use」で始めること
  • カスタムフックの中では、他のフックを呼び出すことができるが、条件分岐やループなどの中では呼び出さないこと
  • カスタムフックはJavaScriptの関数なので、引数や戻り値を自由に設定できる

例えば、以下のコードは、useFetchDataというカスタムフックを作っています。このカスタムフックは、引数として渡されたURLからデータを取得して、状態として保持する機能を提供します。

hooks/useFetchData.js
import { useState, useEffect } from 'react';

const useFetchData = (url) => {
  const [data, setData] = useState([]);
  useEffect( () => {
    let ignore = false;
    const fetchPost = async () => {
      const response = await fetch(url);
      const data = await response.json();
      if (!ignore) {
        setData(data);
      }
    };
    fetchPost();
    return () => {
      ignore = true;
    };
  }, [url]);
  return { data };
};

export default useFetchData;

カスタムフックの利用。

App.js
import useFetchData from './hooks/useFetchData';

const User = () => {
  const { data } = useFetchData('[6](https://xxxxx.jp/users)');
  return (
    <div>
      <h1>ユーザ一覧</h1>
      <ul>
        {data.map( (user) => (
          <li key= {user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default User;

以上です。
///

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?