ReactコンポーネントでWebAPIからデータを取得し画面に表示するまで


はじめに

先日useEffectについてまとめた記事を書きました。本記事では、こちらの記事に収まりきらなかった、useEffectを使って初回レンダリング時にWebAPIからデータを取得して表示する法方と勉強し始めの頃にハマったことを文章に起こします。

この記事ではWebAPIから投稿データを取得し、リスト形式に並べてレンダリングするまでを、筆者の過去の失敗を交え解説していきます。

今回WebAPIから何らかのデータを取得するにあたって、JSONPlaceholderというサービスを利用しました。手軽にREST API を試せるのでおすすめです。

このようなデータが取得できます

事前準備

型定義

WebAPIから取得したデータから利用する要素だけを型定義します。

types.ts
export type Post = {
  id: string;
  body: string;
  title: string;
};

fetch用関数

fetchメソッドを使って https://jsonplaceholder.typicode.com/posts/?_limit=10 にあるリソースを見に行きます。postsを最大10件取得する関数です。
こちらの関数をこの記事の例では使いまわしていきます。

fetchPost.ts
const fetchPosts = async (): Promise<Post[]> => {
  console.log("fetching-start");

  const res = await fetch(
    "https://jsonplaceholder.typicode.com/posts/?_limit=10"
  );

  console.log("fetching-finish");
  return res.json();
};

fetch は「目的の場所に行って取ってくる」という意味があるので外部リソースから取得する時によく使われます。

実践編

バッドパターン

fetch結果がレンダリングされません。

BadPattern.tsx
const PostList: React.FC = () => {
  let posts: Post[] = [];
  fetchPosts().then((data) => {
    posts = data;
  });

  return (
    <div>
      <p>データ一覧</p>
      <ul>
        {console.log("render")}
        {posts.map((post) => {
          return (
            <li key={post.id}>
              <h1>{post.title}</h1>
              <p>{post.body}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

みるからに酷いコードですね...

ですが私は昔本当にこのコードを書いていたのです。本当か疑わしくなってきました。

ブラウザで確認すると、データ一覧の下に投稿データが表示される想定でしたが1件も表示されません。

コードはさておき、なぜレンダリングされないのかを処理の順番を追って見ていきます。

こちらは単に非同期通信処理のハンドリングがされていないからです。
コンポーネント内での処理の順番としては、

  1. fetchPosts関数を呼び出し'fetching-start'のログが吐かれ、WebAPIとの通信が始まります
  2. 画面がレンダリングされる
  3. WebAPIとの通信が終わり、'fetching-finish'のログが測れる

2.のレンダリング時にはまだ投稿データの取得が完了しておらず、ローカル変数postsは空のままなので空のデータがレンダリングされるからです。
なのでuseEffectを使って投稿データをローカル変数に格納し終えたら再度レンダリングしてもらうようにします。

OK パターン

あくまでも私がこれで大丈夫というコードです。動きはします。

OKPattern.tsx
const PostList: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    (async function () {
      const data = await fetchPosts();

      setPosts(data);
      setIsLoading(false);
    })();
  }, []);

  return (
    <div>
      {console.log("render")}
      <p>データ一覧</p>
      {isLoading && <h2>Loading...</h2>}
      <ul>
        {posts.map((post) => {
          return (
            <li key={post.id}>
              <h1>{post.title}</h1>
              <p>{post.body}</p>
            </li>
          );
        })}
      </ul>
    </div>
  );
};

レンダリングの順番をわかりやすくするためにWebAPIとの通信中とのことをユーザーに教えるLoading状態をコンポーネントに追加しました。

ブラウザで確認してみると

ブラウザのリロードボタンを押すと始めにLoading... が表示されて、少しすると投稿データが表示されます。

処理の順番を追っていきます。

  1. 初回レンダリング処理が走ります。
    • この時点で画面にレンダリングされるのはLoading...です
  2. 初回レンダリング後にuseEffect内部の処理が始まります
  3. const data = await fetchPosts() の行で WebAPIとの通信が始まり、'fetching-start'のログが吐かれます
    • 非同期処理なのでfetchPosts()の処理が終了するまで次の処理を待ってくれます
  4. WebAPIから投稿データを取得する処理が完了したので'fetching-finish'のログが吐かれます
  5. ReactコンポーネントのuseEffectに戻り、取得した投稿データをローカル変数postsにセットします
  6. useEffect内で画面に表示するデータが変わったので再レンダリング処理が始まります

開発者コンソールを見てみると以下のようになります。

最後の'render'が二回出てくるのは'posts'と'isLoading'の二つの変数が変更しているからですね。

最後に

改めて過去のコードを眺めると酷いもんだなと思えるようになってきたのは成長の証だと捉えて精進していきます...

以上です。最後まで読んでいただきありがとうございます!