delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

【Reactの勉強】チュートリアルをやってみる(「サーバーからのデータ取得」・「状態の変更」)

概要

前回に引き続き、以下の日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっているチュートリアルをやっていく。

zenn.dev

また、適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)

oukayuka.booth.pm

今回は前回に引き続き「サーバーからのデータ取得」と「状態の変更」のセクションをやる。

Promiseで外部のAPIからデータを取得するところをTypeScriptではどう書けばよいのかでけっこう苦しんだ。

ここはもっといい書き方があるはずなので、後で戻ってきたい。

バージョン

「Reactの勉強」で使用している主なソフトウェアのバージョンは以下の通り。

  • node: 15.5.0
  • react-create-app: 4.0.1
  • react: 17.0.1
  • react-dom: 17.0.1
  • typescript: 4.1.3
  • eslint: 7.17.0
  • prettier: 2.2.1
  • stylelint: 13.8.0

書いたコード(暫定の結論)

いったん以下のように書いてみたところ、とりあえずアプリも動いており、型も定義できている。

ただ、「型アサーション」を使っているところが無理やり感があって、あまりいい書き方ではないのではないかという気がしている。

[api.ts]

type DogAPIRandomResponse = {
  message: string[];
};

const fetchImages = async (breed: string): Promise<string[]> => {
  const response = await fetch(
    `https://dog.ceo/api/breed/${breed}/images/random/12`,
  );

  const data: Promise<DogAPIRandomResponse> = (await response.json()) as Promise<DogAPIRandomResponse>;

  return (await data).message;
};
export default fetchImages;

[App.tsx] ※ 必要な箇所のみ抜粋

const Main: FC = () => {
  const [urls, setUrls] = useState<string[]>([]);
  useEffect(() => {
    void fetchImages('shiba').then((dogImageUrls) => {
      setUrls(dogImageUrls);
    });
  }, []);

  return (
    <main>
      <section className="section">
        <div className="container">
          <Gallery urls={urls} />
        </div>
      </section>
    </main>
  );
};

「サーバーからのデータ取得」の詳細

「サーバーからのデータ取得」のセクションをやった上でつまづいたことを書いていく。
やることとしては、チュートリアルのコードを元にして、TypeScriptを使っているので、型を定義するコードに書き替えていく。

「fetchImages」関数の戻り値の型を定義する。

まずは「fetchImages」関数の戻り値の型を定義する。

「fetchImages」関数は犬の画像のurlのリストを返すので、string[]を定義する。

加えて、「fetchImages」関数はPromiseなので、Promise型で定義する必要がある。
この時点でのコードは以下のようになる。

const fetchImages = async (breed: string): Promise<string[]> => {
  const response = await fetch(
    `https://dog.ceo/api/breed/${breed}/images/random/12`,
  );

  const data = await response.json();

  return data.message;
};

ESLintのエラーを確認する。

関数の戻り値の型は定義したが、以下のコードでそれぞれESLintのエラーが出てしまう。

const data = await response.json();

return data.message;

ESLintのエラーは以下。

f:id:kamatimaru:20210128234639p:plain

f:id:kamatimaru:20210128234651p:plain

エラーメッセージによると以下の2つが原因のようである。

  • await response.json();の戻り値の型はPromise<any>である。
  • 「data」変数はTypeScriptにはany型で認識されているので、「message」というフィールドを持っていることが認識されない。

従って、これらを解消する必要がある。

APIのレスポンスの型を定義する。

原因の一つは、APIのレスポンスの型が定義されていないことなので、api.tsに以下のようにAPIのレスポンスの型を定義する。

type DogAPIRandomResponse = {
  message: string[];
};

※ 実際に返ってくるJSONスキーマは以下のURLをブラウザで開くと分かる。
https://dog.ceo/api/breed/shiba/images/random/12

「data」変数に型を定義する。

上で作成したAPIのレスポンスの型を「data」変数に定義する。
ここで、以下のように単に型を定義するだけだと、ESLintでエラーになってしまう。

const data: Promise<DogAPIRandomResponse> = (await response.json());

ESLintのエラーは以下。

f:id:kamatimaru:20210129004430p:plain

どうすればよいのだろうと思ったが、以下のように『りあクト!』の4章で紹介されている「型アサーション」という手法を使うとESLintのエラーが解消した。

const data: Promise<DogAPIRandomResponse> = (await response.json()) as Promise<DogAPIRandomResponse>;

ただ、本書によると「型アサーション」は最終手段とのことなので、もっとよい方法がないか引き続き調べたいと思う。

「data.message」をreturnする。

上記の対応をしても、まだ以下の行でESLintのエラーが出続けている。

return data.message;

ESLintのエラーは以下。

f:id:kamatimaru:20210129005222p:plain

これに関しては、VS CodeがQuick Fixを提案してくれた。

f:id:kamatimaru:20210129005538p:plain

任せて「Add 'await'」を選択すると、以下のようにコードを修正してくれて、ESLintのエラーも消えた。

return (await data).message;

await dataを括弧で括るのがポイントのようである。
await data.message;と書くとダメである。恐らくawaitが「message」にかかってしまうからだと思う。

これで、ESLintのエラーは全て消えたし、型も定義することができた。
「サーバーからのデータ取得」のセクションに関しては以上。

「状態の変更」の詳細

「状態の変更」セクションでは、上で作成した「fetchImages」関数をApp.tsx側で呼び出して、画像を表示する。
以下に書いたように、何箇所か元のチュートリアルのコードから修正したが、このセクションはそんなに苦労しなかった。

「useState」・「useEffect」が出てきた。

このセクションのコードには「useState」・「useEffect」という関数が登場する。

これらはReactにおいて「Hooks」という重要な機能らしく、『りあクト!』でも9章で1章を割いて説明されている。

ただ、本書の解説も読みながら進めたら、何となくどんな機能なのかは理解できた。

「useState」に型を定義する。

元のコードとの違いとして、TypeScriptでは「useState」に型を渡せるので、以下のように「string[]」と定義する。

const [urls, setUrls] = useState<string[]>(null);

すると、元のコードでは初期値にnullを代入しているが、「string[]」型にはnullを代入できないので、以下のようにESLintでエラーが出る。

f:id:kamatimaru:20210129011708p:plain

ここでの対応として、以下の2つがある。

  1. 空のlistを初期値として渡すように変更する。
  2. nullを代入できるようにする。

2の場合は、『りあクト!』でも紹介されているように、以下のように型をstring[] | null書くと実現できる。

const [urls, setUrls] = useState<string[] | null>(null);

ただ、nullを許容しない方がベターかなと思って、今回は1の方針で対応した。
その場合は、以下のように初期値をnullから空のlistに変更すればよい。

const [urls, setUrls] = useState<string[]>([]);

変数名が重複しているので変える。

元のコードでは、以下のようにuseStateの戻り値を格納する変数と、fetchImagesのコールバック関数の引数の変数の両方で「urls」という変数名を使用している。

const [urls, setUrls] = useState(null);
useEffect(() => {
     fetchImages("shiba").then((urls) => {
       setUrls(urls);
     });
}, []);

コールバック関数の引数の方は仮引数なので、挙動上は問題ないはずだが、ESLintではエラーになってしまう。

f:id:kamatimaru:20210129012805p:plain

従って、以下のように片方の変数名を「dogImageUrls」に変える。

useEffect(() => {
    void fetchImages('shiba').then((dogImageUrls) => {
      setUrls(dogImageUrls);
    });
  }, []);

これでESLintのエラーは全て消えた。

「Loading」を表示する条件を修正する。

「条件分岐」のセクションで以下のように、結果を取得中の状態の場合は、Loadingコンポーネントを表示するようにしていた。

const Gallery: FC<GalleryProps> = (props) => {
  const { urls } = props;
  if (urls.length == null) {
    return <Loading />;
  }

今回、urlsの初期値を空のlistに変更したので、以下のようにlistが空の場合にLoadingコンポーネントを表示するように修正する。

if (urls.length.length === 0) {
  return <Loading />;
}

「状態の変更」のセクションに関しては以上。


成果物

セクションがここまで進むと、毎回APIからランダムに柴犬の画像のURLを取得するようになるので、リロードする度に異なる画像が表示されるようになる。

f:id:kamatimaru:20210129015132p:plain

f:id:kamatimaru:20210129015943p:plain

以上