SWRを使って型定義されたhooksを書いてみる(POST, PUT, DELETE編)
こんにちは。
最近暑くてホットコーヒーが飲めない季節になってきたなぁと感じています。8zkです。
この記事の前にGET編も書きましたので、よければそちらから見ていただくとわかりやすいかと思います。
というわけで今回は採用理由
とSWRとは
は割愛させていただきます。
前回と同様にfetcherが必要なので以下のように定義します。
// /src/helpers/fetcher.ts
import axios from 'axios';
export const fetcher = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
timeout: 10000,
headers: { 'X-Custom-Header': 'foobar' },
});
axios.create
を使ってaxios
のinstanceを作成します。ここではサービス全体で利用するbaseURL
やtimeout
を設定します。
次は作成したfetcher
とuseSWRMutation
を使って各endpointのhooksの元となる実装を作成します。
今回はPOST, PUT, DELETE用です。
データ更新の下準備
// /src/hooks/api/useAPI.ts
import useSWRMutation from 'swr/mutation';
import { AxiosRequestConfig, AxiosError } from 'axios';
import { fetcher } from '@/helpers/fetcher';
const mapUrlParams = (url: string, urlParams?: Record<string, string>) => {
if (urlParams) {
return url.replace(/{([^}]+)}/g, (_, p1) => urlParams[p1]);
}
return url;
};
export const useMutation = <
Request extends {} = {},
Response extends {} = {},
UrlParams extends {} = {},
>(
url: string,
axiosConfig?: AxiosRequestConfig
) => {
return useSWRMutation<
Response,
AxiosError,
string,
Request & { urlParams?: UrlParams }
>(url, (url, fetcherOptions) => {
const {
arg: { urlParams, ...restParams },
} = fetcherOptions;
const mappedUrl = mapUrlParams(url, urlParams);
return fetcher
.request({
url: mappedUrl,
params: restParams,
...axiosConfig,
})
.then((res) => {
return res.data;
});
});
};
今回の実装のキモはmapUrlParams
です。
PUT、DELETEではurlの中に動的なparameterが含まれることがよくあります。それはリソースの更新を行うときにid情報から対象を見つけるためです。例としては/posts/1
のようなendpointです。
今回作成したuseMutation
は引数にurl
を取りますが、urlの中に動的なparameterが含まれるため、以下のようなジレンマが発生します。
const { trigger } = useMutation('/posts/1'); // 1の部分は動的にしたい!
このようなことがよく起こるかと思います。
なのでuseMutation
に/posts/{postId}
としてurlを渡し、trigger
にurlParams
としてpostId
が設定できるようにしました。
そのときに利用するのがmapUrlParams
です。
この関数はurlParams
に含まれるkey(postId
)がurlに含まれる{postId}
と一致した場合にそこをurlParams
のvalueに置き換える正規表現を行います。
結果的にtrigger({ urlParams: { postId: 1 }})
がendpointとして/posts/1
になるのです。
データの更新
import { useMutation } from './useAPI';
type UpdatePostRequest = {
body: string;
title: string;
userId: number;
};
type UpdatePostUrlParams = {
postId: string;
};
type UpdatePostResponse = {
id: number;
};
export const useUpdatePost = () => {
return useMutation<
UpdatePostRequest,
UpdatePostResponse,
UpdatePostUrlParams
>('/posts/{postId}', {
method: 'PUT',
});
};
先ほど作成したuseMutation
を使って型定義を行ないます。
UpdatePostRequest
とUpdatePostUrlParams
とUpdatePostResponse
の型を定義し、それをuseMutation
に渡すことでuseUpdatePost
は型定義されたdata
やtrigger
を利用することができます。
componentでは以下のように利用します。
useMutation
を利用しているためdata
やtrigger
だけでなくerror
やisMutating
やreset
をhooksから利用できます。これらもサービスに応じてハンドリングしてください。
// /src/routes/Home/index.tsx
import { AxiosError } from 'axios';
import { useUpdatePost } from '@/hooks/api/usePost';
export const Home = () => {
const { trigger: updatePost, isMutating: isUpdatePostMutating } =
useUpdatePost();
return (
<div>
<h1>Home</h1>
<button
disabled={isUpdatePostMutating}
onClick={async () => {
try {
await updatePost({
body: 'body',
title: 'title',
userId: 1,
urlParams: { postId: '1' },
});
} catch (e: unknown) {
if (e instanceof AxiosError) {
console.error(e.message);
}
}
}}
>
Update Post
</button>
</div>
);
};
このようにcomponent側で型定義されたSWRのhooksを利用できるようになりました。
前回のGET編も合わせると以下になります。
// /src/hooks/api/useAPI.ts
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
import { AxiosRequestConfig, AxiosError } from 'axios';
import { fetcher } from '@/helpers/fetcher';
const mapUrlParams = (url: string, urlParams?: Record<string, string>) => {
if (urlParams) {
return url.replace(/{([^}]+)}/g, (_, p1) => urlParams[p1]);
}
return url;
};
// For query(only supports 'GET') request
export const useQuery = <Request extends {} = {}, Response extends {} = {}>(
url: string,
params?: Request,
axiosConfig?: AxiosRequestConfig
) => {
const key = [url, JSON.stringify(params)];
return useSWR<Response, AxiosError>(key, () =>
fetcher
.request({
url,
method: 'GET',
params,
...axiosConfig,
})
.then((res) => res.data)
);
};
// For mutation(supports 'POST', 'PUT', 'DELETE') request
export const useMutation = <
Request extends {} = {},
Response extends {} = {},
UrlParams extends {} = {},
>(
url: string,
axiosConfig?: AxiosRequestConfig
) => {
return useSWRMutation<
Response,
AxiosError,
string,
Request & { urlParams?: UrlParams }
>(url, (url, fetcherOptions) => {
const {
arg: { urlParams, ...restParams },
} = fetcherOptions;
const mappedUrl = mapUrlParams(url, urlParams);
return fetcher
.request({
url: mappedUrl,
params: restParams,
...axiosConfig,
})
.then((res) => {
return res.data;
});
});
};
// /src/hooks/api/post.ts
import { useQuery, useMutation } from '@/hooks/api/useAPI';
type Post = {
id: number;
body: string;
title: string;
userId: number;
};
type FetchPostsResponse = Post[];
type FetchPostResponse = Post;
type UpdatePostRequest = {
body: string;
title: string;
userId: number;
};
type UpdatePostUrlParams = {
postId: string;
};
type UpdatePostResponse = {
id: number;
};
type DeletePostUrlParams = {
postId: string;
};
type CreatePostRequest = {
body: string;
title: string;
userId: number;
};
type CreatePostResponse = Post;
export const useFetchPosts = () => {
return useQuery<{}, FetchPostsResponse>(`/posts`, {});
};
export const useFetchPost = (postId: string) => {
return useQuery<{}, FetchPostResponse>(`/posts/${postId}`, {});
};
export const useUpdatePost = () => {
return useMutation<
UpdatePostRequest,
UpdatePostResponse,
UpdatePostUrlParams
>('/posts/{postId}', {
method: 'PUT',
});
};
export const useDeletePost = () => {
return useMutation<{}, {}, DeletePostUrlParams>('/posts/{postId}', {
method: 'DELETE',
});
};
export const useCreatePost = () => {
return useMutation<CreatePostRequest, CreatePostResponse>('/posts', {
method: 'POST',
});
};
以上でGET, POST, PUT, DELETEができるhooksが完成しました。
私はSWRを触っていて漠然とサンプルを眺めていてもどのようにプロダクションで利用するかイメージが湧きませんでしたが、実際に触ってみてプロダクションでも利用できるようなものが作れたんじゃないかなと思います(あくまで希望です)。
これが誰かの参考になったら嬉しいです。
まだまだ型が弱い箇所(特にuseMutation
のurlParams
周り)もありますが、今後も改善を続けていきたいと思います。
最後に
スペースマーケットでは一緒に働く仲間を募集しています!
軽く話を聞いてみたいなという方も大歓迎ですので、興味を持っていただけた方はご応募お待ちしています!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion