「フック」「単純な関数」のような「実装手段」でディレクトリを分けるべきでない
いわゆる「スクリーミングアーキテクチャ(叫ぶアーキテクチャ)」とか「コロケーション原則」を目指す・守るべき
「実装手段」のみに着目して分割するのは、それに真っ向から反するアンチパターン
ただし、コード生成のためにコードを読ませるタイプのツールなど、技術的制約から仕方なく同種のファイルをまとめるのは仕方がない
例: aspida に読ませる型定義ファイル群とか
あと、極めて抽象的なユーティリティは、 utils 直下とか、 /utils/hooks /utils/dom のようなところに分けても良いだろう
特に、「〇〇という関心事のためのユーティリティ」と言うことができないので。
例:safeNullable<A, B>(fn: (a: A) => B) => (value: A | undefined | null): B | undefined
null | undefined を受け取ったら undefined を返す
そうでない場合は fn(value) を実行してその結果を返す
詳細:
Rules of Hooks 等の技術的な制限によって「やっぱり単純な関数に切り出すべきだった」が十分ありえる。
そのたびに /hooks → /utils のようにファイルを移動するなんてアホらしい
一つの関心事が、「単純な関数」「型」「値」「フック」とかをそれぞれ export することで成り立つことが十分ありえる。それらは一箇所にまとめるべき。
知識として互いに強い関連性があるファイルを、 /hooks /consts /types と離れ離れにするなんてアホらしい
例: 「曜日」という関心事
以下の記述・ファイルたちを /utils/day-of-week のようなディレクトリにまとめるのが良さそう
type DayOfWeek = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat" という型
入力文字列がそれに合うか検証する関数(zod や valibot 等のスキーマでもよい)
選択肢一覧が入った配列
他の例: TanStack Query (React Query) を使ったデータフェッチをレイヤー化したときの置き場所
❌️ 悪い例: 種類によって分ける
(queryOptions は使わない、v4 の使い方の例)
useQuery をラップしたカスタムフックはフックだから /hooks
クエリキーは、フックでもコンポーネントでもない単純な関数だから /utils
queryFn で呼び出される非同期関数も同じところに入れる
→ めちゃくちゃ。これを整理したとは言えない。
code:@/hooks/use-posts.ts
import { useQuery } from "@tanstack/react-query;
import { postQueryKeys } from "@/utils/postQueryKeys";
import { getPosts } from "@/utils/getPosts";
export const usePosts = (page: number, limit: number) => {
return useQuery({
queryKey: postQueryKeys.lists(page, limit),
queryFn: getPosts,
})
}
✅️ 良い例: 「Web API を呼び出してデータを取得・更新する」ためのモジュール類をまとめたディレクトリ
ここでは、queryOptions オブジェクトや、key を得られる関数を一つのファイルにまとめて置いている。
queyOptions に渡す非同期関数は、同じディレクトリ、近くに置いている。
ちなみに、このように非同期関数を queryOptions とは別で export しておくと、useQuery を介さず直に呼び出す必要がある場合にも対応できる。
ディレクトリ構造
📂 @/entities/post/api/
├ post.queries.ts
├ get-posts.ts
└ get-detail-post.ts
code:@/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 = {
list: (page: number, limit: number) =>
queryOptions({
queryFn: () => getPosts(page, limit),
placeholderData: keepPreviousData,
}),
detail: (query?: PostDetailQuery) =>
queryOptions({
queryFn: () => getDetailPost({ id: query?.id }),
staleTime: 5000,
}),
};