概要
前回に引き続き、以下の日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっているチュートリアルをやっていく。
また、適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)
今回は前回に引き続き「サーバーからのデータ取得」と「状態の変更」のセクションをやる。
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のエラーは以下。
エラーメッセージによると以下の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のエラーは以下。
どうすればよいのだろうと思ったが、以下のように『りあクト!』の4章で紹介されている「型アサーション」という手法を使うとESLintのエラーが解消した。
const data: Promise<DogAPIRandomResponse> = (await response.json()) as Promise<DogAPIRandomResponse>;
ただ、本書によると「型アサーション」は最終手段とのことなので、もっとよい方法がないか引き続き調べたいと思う。
「data.message」をreturnする。
上記の対応をしても、まだ以下の行でESLintのエラーが出続けている。
return data.message;
ESLintのエラーは以下。
これに関しては、VS CodeがQuick Fixを提案してくれた。
任せて「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でエラーが出る。
ここでの対応として、以下の2つがある。
- 空のlistを初期値として渡すように変更する。
- 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ではエラーになってしまう。
従って、以下のように片方の変数名を「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 />; }
「状態の変更」のセクションに関しては以上。