LoginSignup
430
346

More than 5 years have passed since last update.

React Hooksでデータを取得する方法

Last updated at Posted at 2019-06-07

Robin Wieruch氏によるHow to fetch data with React Hooks?を著者の許可を得て意訳しました。
誤りやより良い表現などがあればご指摘頂けると助かります。

原文: https://www.robinwieruch.de/react-hooks-fetch-data/


このチュートリアルでは、ステートフック副作用フックでデータを取得する方法を解説します。テック系の人気記事を取得するためによく使われるHacker News APIを利用します。また、アプリケーション内の任意の場所で再利用したり、スタンドアロンのnodeパッケージとしてnpmに公開したりできるデータ取得用のカスタムフックも実装します。

React のこの新機能が初見であれば、まずReact Hooks入門に目を通してください。完成したプロジェクトでReact Hoos でのデータ取得事例を確認したければ、このGitHub リポジトリをどうぞ。

データ取得用の手軽な React フックが欲しいだけであれば、 npm install use-data-api してドキュメントに従ってください。導入するのであればスターを付けるのも忘れずに^^

注: 将来的には、React Hooks はデータ取得を目的としたものにはなりません。代わりに Suspense という機能がそれを担います。それでも、以下のチュートリアルは state と副作用フックについて習熟するための素晴らしい方法です。

React Hooks によるデータ取得

React でのデータ取得に不慣れであれば、Reactでのデータ取得大全に目を通すことをオススメします。React クラスコンポーネントでのデータ取得、レンダープロップコンポーネントHOCsによってコンポーネントを再利用する方法、そしてエラーハンドリングとローディングスピナーの処理法について学ぶことができます。この記事では、それら全てを関数コンポーネント内の React Hooks で実装します。

import React, { useState } from 'react';

function App() {
  const [data, setData] = useState({ hits: [] });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

App コンポーネントに項目の一覧(Hacker News での検索にヒットした記事)が表示されます。state およびその更新関数は、 useState というステートフックから取得されます。これは App コンポーネント用に取得するデータのローカル state を管理します。初期 state はデータを示すオブジェクト内の hits に空配列が含まれます。このデータにはまだ誰も state を設定していません。

このチュートリアルではaxiosを使ってデータ取得を行いますが、他のデータ取得ライブラリや、ブラウザネイティブの fetch API を使っても構いません。axios をまだインストールしていないのであれば、コマンドラインで npm install axios してください。準備ができたらデータ取得用の副作用フックを実装していきましょう。

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

function App() {
  const [data, setData] = useState({ hits: [] });

+  useEffect(async () => {
+    const result = await axios(
+      'http://hn.algolia.com/api/v1/search?query=redux',
+    );
+
+    setData(result.data);
+  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

useEffect という副作用フックは、API から axios でデータを取得し、ステートフックの更新関数によってコンポーネントのローカル state にデータを設定します。promise は async/await によって解決されます。

しかし、アプリケーションを実行すると厄介なループに陥るでしょう。副作用フックはコンポーネントのマウント時だけでなく、更新時にも実行されます。データを取得するたびに state を設定しているため、コンポーネントが更新されて副作用が再び実行されるからです。データ取得を何度も繰り返してしまいます。これはバグなので回避する必要があります。コンポーネントのマウント時にだけデータを取得するようにしましょう。 副作用フックの第2引数に空配列を渡すことで、コンポーネント更新時ではなくマウント時にだけ有効化することができます。

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

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      'http://hn.algolia.com/api/v1/search?query=redux',
    );

    setData(result.data);
+  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

第2引数はフックが依存する全ての変数(この配列に割り当てられている)を定義するために使われます。その変数が更新されるとフックは再度実行されます。変数の配列が空であれば、フックはコンポーネントの更新時に実行されません。変数を監視する必要がないためです。

最後にもう一点。コード内でサードパーティの API からデータを取得するために async/await を使用しています。ドキュメントによると、asyncアノテーションが付けられた関数はいずれも暗黙の promise を返します。「async 関数宣言は非同期関数を定義します。これは非同期関数オブジェクトを返します。非同期関数は、結果を返すために暗黙的な Promise を使ってイベントループ経由で非同期で動作する関数です。」しかし、副作用フックは何も返さないか、クリーンアップ関数を返すべきです。そのため、開発者コンソールには次の警告が表示されるかもしれません。07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect. useEffect 関数内で直接 async を使用することはできませんので、副作用内で async 関数を使うことで回避しましょう。

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

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
+    const fetchData = async () => {
+      const result = await axios(
+        'http://hn.algolia.com/api/v1/search?query=redux',
+      );

+      setData(result.data);
+    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

これが React Hooks を使った簡単なデータ取得です。しかし、エラーハンドリング、ローディングインジケータ、フォームからデータ取得を実行する方法や再利用可能なデータ取得フックの実装方法などに興味があればそのまま読み進めてください。

プログラムもしくは手動でフックをトリガーするには?

上手いことコンポーネントのマウント時に一度だけデータを取得できました。しかしどの話題に関心があるのかをAPIに伝えるためにインプットフィールドを使うにはどうすれば良いのでしょうか?「Redux」がデフォルトの query として設定されていますが、「React」の話題に関心があるとしたら?「Redux」以外の話題を取得できるようにインプット要素を実装してみましょう。まずインプット要素に新しい state を導入します。


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

function App() {
  const [data, setData] = useState({ hits: [] });
+  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://hn.algolia.com/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <Fragment>
+      <input
+        type="text"
+        value={query}
+        onChange={event => setQuery(event.target.value)}
+      />
      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

export default App;

現時点では、どちらの state もお互いに独立していますが、それらを結合して、インプットフィールドの query で指定した記事だけを取得するようにしてみましょう。次のような変更により、コンポーネントはマウント時に query でヒットした記事だけを取得するようになります。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
+        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    ...
  );
}

export default App;

1つ考慮もれがあります。マウント後にインプットフィールドに何かを入力しても、副作用でデータを取得することができていません。副作用の第2引数に空配列を渡しているためです。副作用は変数に依存していないため、コンポーネントのマウント時にだけ実行されます。しかし、副作用は query に依存するべきです。query の更新によってデータリクエストが再度実行されます。


...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
+  }, [query]);

  return (
    ...
  );
}

export default App;

インプットフィールドの更新に応じてデータの再取得が実行されるはずです。しかしここでまた別の問題があります。一文字入力するたびに副作用によってデータ取得リクエストが実行されてしまいます。ボタンを用意して手動でフックを実行してリクエストするようにしましょう。

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
+  const [search, setSearch] = useState('');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
+      <button type="button" onClick={() => setSearch(query)}>
+        Search
+      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

ここで、副作用はインプットフィールドで一文字入力するたびに更新される query state ではなく search state に依存するようにします。ユーザーがボタンをクリックすると、新しい search state が設定されて、手動で副作用フックが実行されるはずです。


...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
+  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
+        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
+  }, [search]);

  return (
    ...
  );
}

export default App;

また、search state の初期値は query state と同じ値になります。コンポーネントがマウント時にもデータ取得を実行するため、結果はインプットフィールドに反映されます。しかし、よく似た query と search state は混乱の元です。search state の代わりに実際のURL を設定してみましょう。

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
+  const [url, setUrl] = useState(
+    'http://hn.algolia.com/api/v1/search?query=redux',
+  );

  useEffect(() => {
    const fetchData = async () => {
+      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
+  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
+          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

これが暗黙的なプログラムでのデータ取得を副作用フックで実装した例になります。副作用がどの state に依存するかを決めることができます。クリックもしくは別の副作用でこの state を設定すると、この副作用が再度実行されます。この例では、URL state が更新されると副作用が再度実行されて API から話題を取得します。

React Hooks によるローディングインジケータ

データ取得にローディングインジケータを導入しましょう。これは単純にステートフックで管理される別の state です。ローディングフラグは、App コンポーネントでローディングインジケータをレンダーするために使われます。

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

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
+  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
+      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
+      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

+      {isLoading ? (
+        <div>Loading ...</div>
+      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
+      )}
    </Fragment>
  );
}

export default App;

コンポーネントのマウント時もしくは URL state が更新されたタイミングでデータ取得用の副作用が実行されると、loading state は true になります。リクエストが解決されると、loading state は再び false になります。

React Hooks によるエラーハンドリング

React Hooks でのデータ取得に対するエラーハンドリングについても学んでいきましょう。エラーはステートフックで初期化されるまた別の state に過ぎません。isError state が true の時、App コンポーネントはユーザーにフィードバックを提供することができます。async/await を導入しているのであれば、エラーハンドリングのために try/catch ブロックを使うのが一般的です。副作用内に記述していきましょう。


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

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
+  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
+      setIsError(false);
      setIsLoading(true);

+      try {
        const result = await axios(url);

        setData(result.data);
+      } catch (error) {
+        setIsError(true);
+      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={event => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

+      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

isError state はフックが再実行されるたびにリセットされます。失敗したリクエストの後にユーザーがもう一度試行すると isError がリセットされるので便利です。自らエラーを強制するために URL を無効なものに変更することができますので、エラーメッセージが表示されることを確認してください。

フォームと React によるデータ取得

フォームを使ったデータ取得についてもやっていきましょう。今までのところ、インプットフィールドとボタンの組み合わせしかありません。インプット要素が増えてくると、フォーム要素でラップしたくなるかもしれません。また、フォームはキーボードの「エンター」でボタンを実行することも可能です。


function App() {
  ...

  return (
    <Fragment>
+      <form
+        onSubmit={() =>
+          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
+        }
+      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
+        <button type="submit">Search</button>
+      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

しかし現状では、送信ボタンを押すとブラウザがリロードしてしまいます。これはフォームを送信するときの本来の動作です。標準動作を防ぐために、React イベントで関数を呼び出すことができます。これは React クラスコンポーネントでも同様です。


function App() {
  ...

  return (
    <Fragment>
+      <form onSubmit={event => {
        setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);

+        event.preventDefault();
+      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

これで送信ボタンを押してもブラウザがリロードすることはなくなりました。これまで通り動作しますが、今回はネイティブのインプットフィールドとボタンの組み合わせの代わりにフォームを使用します。キーボードの「エンター」キーを押すこともできます。

データ取得用カスタムフック

データ取得用のカスタムフックを抽出するため、インプットフィールドに属している query state を除く、ローディングインジケータやエラーハンドリングなどデータ取得に関する全てを独自の関数に移動させます。App コンポーネントで使われるその関数から全ての必要な変数を返すことを忘れないでください。

+ const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'http://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

+  return [{ data, isLoading, isError }, setUrl];
+ }

これで新しいフックが App コンポーネントから利用できるようになりました。

function App() {
  const [query, setQuery] = useState('redux');
+  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();

  return (
    <Fragment>
      <form onSubmit={event => {
+        doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);

        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      ...
    </Fragment>
  );
}

初期 state も設定可能です。単純に新しいカスタムフックに値を渡します。

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

+ const useDataApi = (initialUrl, initialData) => {
+  const [data, setData] = useState(initialData);
+  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

function App() {
  const [query, setQuery] = useState('redux');
+  const [{ data, isLoading, isError }, doFetch] = useDataApi(
+    'http://hn.algolia.com/api/v1/search?query=redux',
+    { hits: [] },
  );

  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(
            `http://hn.algolia.com/api/v1/search?query=${query}`,
          );

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map(item => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

カスタムフックでのデータ取得については以上になります。フック自体は API について何も知りません。全てのパラメータを外部から受け取り、data、loading や error のような state を必要に応じて管理するだけです。リクエストを処理し、データ取得用のカスタムフックとして data をコンポーネントに返します。

データ取得用 reducer フック

これまで様々なステートフックで、データ取得のための data、loading や error state を管理してきました。しかし、どういうわけかこれら全ての state は、自身のステートフックでまとめて管理されていますが、これらが同じ関心事を持っているためです。ご覧のとおり、これらは全てデータ取得関数内で利用されています。state をまとめるかどうかの良い指標は、逐次実行される(例. setIsErrorsetIsLoading)かどうかです。これら全3種をReducerフックで統合してみましょう。

Reducer フックは state オブジェクトとその更新関数を返します。dispatch 関数と呼ばれるその関数は、type と任意の payload を持つ action を引数に取ります。この全ての情報は、実際の reducer 関数に使われ、以前の state、action の任意の payload と type から新しい state を生成します。コード内でどのように動作するのか見ていきましょう。

import React, {
  Fragment,
  useState,
  useEffect,
+  useReducer,
} from 'react';
import axios from 'axios';

+ const dataFetchReducer = (state, action) => {
+  ...
+ };

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

+  const [state, dispatch] = useReducer(dataFetchReducer, {
+    isLoading: false,
+    isError: false,
+    data: initialData,
+  });

  ...
};

Reducer フックは reducer 関数と初期 state をパラメータとして取ります。今回は、引数として渡す data、loading そして error state は変更されていませんが、それらは単一のステートフックではなく、1つの reducer フックで管理される1つの state オブジェクトに集約されています。

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
+      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

+        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
+        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [url]);

  ...
};

データ取得時に、dispatch 関数から reducer 関数に情報を送ることができます。dispatch 関数によって送られるオブジェクトは、必須の type プロパティと任意の payload プロパティを持ちます。type はどの state の遷移が必要であるかを指示し、payload は新しい state を生成するための追加情報として reducer 関数に渡されます。最終的に、3種の state の遷移が必要となります。データ取得処理の初期化、成功の通知そしてエラーの通知です。

カスタムフックの末尾で、以前のように state を返しますが、これは state オブジェクトが独立したものではなくなったためです。このように useDataApi カスタムフックを呼び出すことで dataisLoading そして isError にアクセスできるようになります。

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

+  return [state, setUrl];
};

大事なことを言い忘れましたが、reducer 関数が未実装です。FETCH_INITFETCH_SUCCESSFETCH_FAILURE の3種の state 遷移に応じた振る舞いが必要です。それぞの state 遷移は新しい state オブジェクトを返します。switch case 文で実装する方法について見ていきましょう。

const dataFetchReducer = (state, action) => {
+  switch (action.type) {
+    case 'FETCH_INIT':
+      return { ...state };
+    case 'FETCH_SUCCESS':
+      return { ...state };
+    case 'FETCH_FAILURE':
+      return { ...state };
+    default:
+      throw new Error();
+  }
};

reducer 関数は引数を経由して現在の state と次の action にアクセスできます。現状の switch case 文では、各 state 遷移は前の state を返すだけです。分割代入は state オブジェクトをイミュータブルに保つために使用されます。つまり、state は決して直接変更されることはなく、ベストプラクティスに従うことを強制します。それでは、state 遷移ごとに state を更新するために、現在の state から返されるプロパティのいくつかを上書きしましょう。

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state,
+        isLoading: true,
+        isError: false
      };
    case 'FETCH_SUCCESS':
      return {
        ...state,
+        isLoading: false,
+        isError: false,
+        data: action.payload,
      };
    case 'FETCH_FAILURE':
      return {
        ...state,
+        isLoading: false,
+        isError: true,
      };
    default:
      throw new Error();
  }
};

これで action の type によって決められた各 state 遷移が前の state と任意の payload に基づいた新しい state を返します。たとえば、リクエスト成功時には、payload が新しい state オブジェクトの data に設定されます。

結論として、Reducer フックは state 管理のこの部分が独自のロジックでカプセル化されていることを保証します。action type と任意の payload を与えることで、常に予測可能な state の更新が発生します。さらに、無効な state に遭遇することは決してありません。たとえば、以前は誤って isLoadingisError を true にすることも可能でした。この場合は UI に何を表示する必要があるでしょうか?reducer 関数によって定義された各 state 遷移は有効な state オブジェクトになりますので、もう心配無用です。

副作用フックでのデータ取得キャンセル

React におけるよくある問題として、コンポーネントが既にアンマウントされている(例. React Router で別ページに遷移した時)にも関わらず、コンポーネントの state は設定されていることがあります。この問題については、様々なシナリオでアンマウントされたコンポーネントの state が設定されるのを防ぐ方法で詳細を書きました。データ取得用のカスタムフックで state を設定できないようにする方法についても見てみましょう。

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
+    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

+        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
+        }
      } catch (error) {
+        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' });
+        }
      }
    };

    fetchData();

+    return () => {
+      didCancel = true;
+    };
  }, [url]);

  return [state, setUrl];
};

全ての副作用フックには、コンポーネントがアンマウントされた時に実行されるクリーンアップ関数を付与することができます。クリーンアップ関数はフックから返される関数です。この例では、didCancel と呼ばれる boolean フラグを使って、データ取得ロジックにコンポーネントの状態(マウントされた/アンマウントされた)を知らせます。コンポーネントがアンマウントされた時は、フラグが true に設定され、データ取得が非同期で解決された後にコンポーネントの state を設定できないようにしています。

注:実際にデータ取得がキャンセルされるわけではありません。これはAxios Cancellationで実現可能ですが、アンマウントされたコンポーネントの state 遷移は実行されなくなっています。個人的に Axios Cancellation の API はベストとは思えないので、この boolean フラグで state の更新を同じように防いでいます。

React のデータ取得で React Hooks によってステートと副作用を扱う方法を学びました。クラスコンポーネント(および関数コンポーネント)でレンダープロップやHoCsを使ってデータ取得する方法についても興味があれば、私の他の記事を最初から読んでみてください。

また、この記事が React Hooks についての学習や、実際のシナリオでの活用法として役立つことを願っています。

430
346
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
430
346