🐥

APIをリクエストするCustom HooksをTypeScriptで書いてみよう

2021/01/25に公開

書くこと

  • APIをリクエストする処理をCustom Hooksとして書く(axiosを使用してGetリクエスト)。
  • TypeScriptで記述して型定義をする。

まずはJavaScriptでCustom Hooksを作成する

作成するCustom Hooksの概要

  • 同じ会社に所属するユーザー全てを取得するAPIにリクエスト。
  • 引数としてcompanyIdを受け取る。
  • 返り値としてdata, error, loadingをkeyとしたオブジェクトを返す。
  • dataはkeyをusers, valueは配列の中にオブジェクトの形で返す。
dataサンプル
{ users: [
    { family_name: 'Endou', first_name: 'Shiki', company_id: 3 }, 
    { family_name: 'Tanaka', first_name: 'Tarou', company_id: 3 },
    ...
  ] 
}
hooks/useFetchUsers.js
import axios from 'axios'
import { useEffect, useState } from "react";

export const useFetchUsers = (companyId) => {
  // useStateでレスポンスの状態を管理する。
  const [res, setRes] = useState({ data: null, error: null, loading: false })

  // useFetchUsersを呼び出したときにfetchRequest関数を実行。
  useEffect(() => {
    fetchRequest(companyId)
  }, [])

  // '/api/v1/users'にリクエストする関数。
  const fetchRequest = (companyId) => {
    // リクエストが返ってくるまでは loading を true。
    setRes(prevState => ({ ...prevState, loading: true }))
    axios.get('/api/v1/users', {
      params: {
        companyId
      }
    }).then((response) => {
      // 成功したら data に users を格納。
      setRes({ data: response.data, error: null, loading: false })
    }).catch(error => {
      console.log(error);
      // 失敗したら error に エラー情報を格納。
      setRes({ data: null, error: error, loading: false })
  }

  return res
}

Custom Hooks を使用する

components/userList.jsx
import React, { useState } from 'react';
import { useFetchUsers } from 'common/hooks/users/use_fetch_users'

const userList = () => {
  // オブジェクトの分割代入, Custom Hooksを実行。
  const { data, error, loading } = useFetchUsers(3)

  if (loading) return <div>...loading</div>
  if (error) return <div>{error.message}</div>

  return (
    // dataの初期値はnullなので && を使用する。
    <>
      { data && data.users.map(user => {
        return (
          <>
            <div>{user.family_name}</div>
            <div>{user.first_name}</div>
            <div>{user.company_id}</div>
          </>
        )
      }) }
    </>
  )
}

JavaScriptからTypeScriptに変更する

hooks/useFetchUsers.ts
 // axiosにerrorの型定義があるのでimport。
+ import axios, { AxiosError } from 'axios'
 import { useEffect, useState } from "react";

  // dataの型定義。
+ interface IUser {
+   users: Array<{
+     family_name: string;
+     first_name: string;
+     company_id: number;
+   }>
+ }

  // レスポンスの型定義。
+ interface IResponse {
+   data: IUser;
+   error: AxiosError;
+   loading: boolean;
+ }

- export const useFetchUsers = (companyId) => {
+ export const useFetchUsers = (companyId: number) => {
  // ① 明示的な型指定が必要。
- const [res, setRes] = useState({ data: null, error: null, loading: false })
+ const [res, setRes] = useState<IResponse>({ data: null, error: null, loading: false })

  useEffect(() => {
    fetchRequest()
  }, [])

  const fetchRequest = () => {
    setRes(prevState => ({ ...prevState, loading: true }))
    // ② レスポンスの型を指定できるのでIUserを指定する。
-   axios.get('/api/v1/users', {
+   axios.get<IUser>('/api/v1/users', {
      params: {
        companyId,
      },
    }).then((response) => {
      setRes({ data: response.data, error: null, loading: false })
-   }).catch(error => {
+   }).catch((error: AxiosError) => {
      console.log(error);
      setRes({ data: null, error, loading: false })
    })
  }

  return res
}

data, errorの初期値がnullなので型推論に任せるとanyになってしまう

型推論に任せたケース
const [res, setRes] = useState({ data: null, error: null, loading: false })
// resの型定義 { data: any, error: any, loading: boolean } 

axios.getGenericsが使用されているのでレスポンスの型を指定できる

axios.getの型定義
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;

https://github.com/axios/axios/blob/fe52a611efe756328a93709bbf5265756275d70d/index.d.ts#L140

TypeScriptで書いたCustom Hooksを使用してみよう

  • tsファイルをimportするので拡張子をtsxに変更。
components/userList.tsx
import React, { useState } from 'react';
import { useFetchUsers } from 'common/hooks/users/use_fetch_users'

const userList = () => {
  // ① data, error, loading にhoverをすると型情報を参照できる。
  const { data, error, loading } = useFetchUsers(3)

  if (loading) return <div>...loading</div>
  if (error) return <div>{error.message}</div>

  return (
    // ② 'user.age'のような型定義されていないプロパティを参照するとエラーが表示される。
    <>
      { data && data.users.map(user => {
        return (
          <>
            <div>{user.family_name}</div>
            <div>{user.first_name}</div>
            <div>{user.company_id}</div>
            <div>{user.age}</div>
          </>
        )
      }) }
    </>
  )
}

data, error, loading, data.users にカーソルをhoverすると型情報が表示される

表示される型情報
data: IUser

error: AxiosError

loading: boolean

data.users: IUser.users: {
    family_name: string;
    first_name: string;
    company_id: number;
}[]

data.usersに定義されていないプロパティを参照するとエディタ上でエラーが表示される

型安全
data.users[0].age
// エラーメッセージ 
`Property 'age' does not exist on type '{ family_name: string; first_name: string; company_id: number; }'.`

間違っている箇所や他にもっと良い書き方があればコメントして頂けると嬉しいです。

Discussion