React Queryとの併用
キーをどこに置くか問題
解決策 - エンティティごとに分割する
プロジェクトにすでにエンティティの分割があり、各クエリが1つのエンティティに対応している場合、エンティティごとに分割するのが最良です。この場合、次の構造を使用することをお勧めします。
└── src/ #
├── app/ #
| ... #
├── pages/ #
| ... #
├── entities/ #
| ├── {entity}/ #
| ... └── api/ #
| ├── `{entity}.query` # クエリファクトリー、キーと関数が定義されている
| ├── `get-{entity}` # エンティティを取得する関数
| ├── `create-{entity}` # エンティティを作成する関数
| ├── `update-{entity}` # オブジェクトを更新する関数
| ├── `delete-{entity}` # オブジェクトを削除する関数
| ... #
| #
├── features/ #
| ... #
├── widgets/ #
| ... #
└── shared/ #
... #
もしエンティティ間に関係がある場合(例えば、「国」のエンティティに「都市」のエンティティ一覧フィールドがある場合)、@x
アノテーションを使用した組織的なクロスインポートの実験的アプローチを利用するか、以下の代替案を検討できます。
代替案 — クエリを公開で保存する
エンティティごとの分割が適さない場合、次の構造を考慮できます。
└── src/ #
... #
└── shared/ #
├── api/ #
... ├── `queries` # クエリファクトリー
| ├── `document.ts` #
| ├── `background-jobs.ts` #
| ... #
└── index.ts #
次に、@/shared/api/index.ts
に
@/shared/api/index.ts
export { documentQueries } from "./queries/document";
問題「ミューテーションはどこに?」
ミューテーションをクエリと混合することは推奨されません。2つの選択肢が考えられます。
1. 使用場所の近くにAPIセグメントにカスタムフックを定義する
@/features/update-post/api/use-update-title.ts
export const useUpdateTitle = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, newTitle }) =>
apiClient
.patch(`/posts/${id}`, { title: newTitle })
.then((data) => console.log(data)),
onSuccess: (newPost) => {
queryClient.setQueryData(postsQueries.ids(id), newPost);
},
});
};
2. 別の場所(Shared層やEntities層)にミューテーション関数を定義し、コンポーネント内でuseMutation
を直接使用する
const { mutateAsync, isPending } = useMutation({
mutationFn: postApi.createPost,
});
@/pages/post-create/ui/post-create-page.tsx
export const CreatePost = () => {
const { classes } = useStyles();
const [title, setTitle] = useState("");
const { mutate, isPending } = useMutation({
mutationFn: postApi.createPost,
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) =>
setTitle(e.target.value);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate({ title, userId: DEFAULT_USER_ID });
};
return (
<form className={classes.create_form} onSubmit={handleSubmit}>
<TextField onChange={handleChange} value={title} />
<LoadingButton type="submit" variant="contained" loading={isPending}>
Create
</LoadingButton>
</form>
);
};
クエリの組織化
クエリファクトリー
このガイドでは、クエリファクトリーの使い方について説明します。
注記
クエリファクトリーとは、JSオブジェクトのことで、そのオブジェクトキーの値がクエリキー一覧を返す関数である。
const keyFactory = {
all: () => ["entity"],
lists: () => [...postQueries.all(), "list"],
};
備考
queryOptions
- react-query@v5に組み込まれたユーティリティ(オプション)
queryOptions({
queryKey,
...options,
});
より高い型安全性と将来のreact-queryのバージョンとの互換性を確保し、クエリの関数やキーへのアクセスを簡素化するために、@tanstack/react-query
のqueryOptions
関数を使用することができる(詳細はこちら)。
1. クエリファクトリーの作成
@/entities/post/api/post.queries.ts
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { getPosts } from "./get-posts";
import { getDetailPost } from "./get-detail-post";
import { PostDetailQuery } from "./query/post.query";
export const postQueries = {
all: () => ["posts"],
lists: () => [...postQueries.all(), "list"],
list: (page: number, limit: number) =>
queryOptions({
queryKey: [...postQueries.lists(), page, limit],
queryFn: () => getPosts(page, limit),
placeholderData: keepPreviousData,
}),
details: () => [...postQueries.all(), "detail"],
detail: (query?: PostDetailQuery) =>
queryOptions({
queryKey: [...postQueries.details(), query?.id],
queryFn: () => getDetailPost({ id: query?.id }),
staleTime: 5000,
}),
};
2. アプリケーションコードでのクエリファクトリーの適用
import { useParams } from "react-router-dom";
import { postApi } from "@/entities/post";
import { useQuery } from "@tanstack/react-query";
type Params = {
postId: string;
};
export const PostPage = () => {
const { postId } = useParams<Params>();
const id = parseInt(postId || "");
const {
data: post,
error,
isLoading,
isError,
} = useQuery(postApi.postQueries.detail({ id }));
if (isLoading) {
return <div>Loading...</div>;
}
if (isError || !post) {
return <>{error?.message}</>;
}
return (
<div>
<p>Post id: {post.id}</p>
<div>
<h1>{post.title}</h1>
<div>
<p>{post.body}</p>
</div>
</div>
<div>Owner: {post.userId}</div>
</div>
);
};