Reactとviteで新しくアプリを作るときのログ

普段はReactを使って開発をしているが、ライブラリなどはそこまで新しい感じではない
なので今から新規に作るとどうなるのかという作業のログを残す
どんなアプリにするかだが、ドイツ語を学習しているのでとりあえず単語を永続化できて、それをランダムに表示するようなものを目指す
技術スタックで確定しているのは以下
- React
- Vite
- tailwindcss
あとは必要になったら都度考える。見切り発車

公式を読みながら進める。tailwindcssは4になったからか、導入がめっちゃ楽になっている
tailwindcssとViteのpluginを入れるだけで、導入に必要な依存ライブラリが減っていた

ディレクトリ構造で悩んだけど、今回はそこをやりこみたいわけではないので、Clineにdesign_docを生成してもらった
以下のようなものが生成される
# German Vocabulary Learning App Design Document
## Directory Structure
frontend/
├── public/
│ └── vite.svg
├── src/
│ ├── vocabulary/
│ │ ├── components/
│ │ │ ├── VocabularyList.tsx
│ │ │ └── VocabularyForm.tsx
│ │ ├── hooks/
│ │ │ └── useVocabulary.ts
│ │ ├── services/
│ │ │ └── mockApi.ts
│ │ ├── styles/
│ │ │ └── Vocabulary.css
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── .gitignore
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
## 説明
- **public/**: 静的アセットを含みます。`vite.svg`はその一例です。
- **src/**: アプリケーションのメインソースディレクトリです。
- **vocabulary/**: 語彙機能に関連するすべてのファイルを含みます。
- **components/**: 語彙機能のためのReactコンポーネントです。`VocabularyList.tsx`は保存された語彙のリストを表示し、`VocabularyForm.tsx`は新しい語彙項目の入力を処理します。
- **hooks/**: 語彙機能のためのカスタムReactフックです。`useVocabulary.ts`は語彙項目に関連する状態とロジックを管理します。
- **services/**: 語彙機能に関連するAPIインタラクションのためのサービスファイルです。`mockApi.ts`は語彙管理のためのAPI呼び出しをシミュレートします。
- **styles/**: 語彙機能に特化したCSSファイルです。`Vocabulary.css`は語彙関連のコンポーネントをスタイルします。
- **App.tsx**: メインアプリケーションコンポーネントです。
- **index.css**: アプリケーションのグローバルスタイルです。
- **main.tsx**: アプリケーションのエントリーポイントです。
- **vite-env.d.ts**: ViteのためのTypeScript環境宣言です。
## 理由
この構造はコードを論理的なセクションに整理し、保守と拡張を容易にします。コンポーネント、フック、サービスを分離することで、アプリケーションの各部分を独立して開発およびテストすることができます。`services`ディレクトリを使用することで、将来的にモックAPIを実際のAPIに簡単に置き換えることができます。`styles`ディレクトリは、コンポーネント固有のスタイルを整理し、グローバルスタイルから分離します。

よくよく考えたらstyleはいらないので削除

ボキャブラリーページを作るにあたって、ルーターを決めていないことに気づいた
TanstackRouterにしようと思ったけど、一応v7になったReactRouterもみてみよう

どっちも触ると重そうだったので、いったん切り上げてTanstackRouterを触る
installは簡単。今回はFileBasedRoutingで作る。Next.jsとかもディレクトリ構造とroutingをマッピングしているからこの機会に触れておこう
- routeの型情報が
routeTree.gen.ts
に書き出される。viteの場合はplugin経由で自動更新されるように見える -
outletになじみがなかったけど、ReactRouterにもある概念らしい
- nestしたpathに対応する機能。/fooにOutletを置いて子Componentのroutingを指定していると、/foo/barの時は/fooに対応するComponentを表示、/foo/bazの時は/bazに対応するComponentを表示することができる。ページ内にtabとかで子ページがあるときに便利そうだ

viteにTanStackRouterVite({ autoCodeSplitting: true }),
入れて動かしてるとき、routesにファイル作ると自動でテンプレが挿入される。便利なようなおせっかいなような
// test.tsx
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/test")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/test"!</div>;
}

file basedなroutingについて理解を深めたい。なんか脱線してる気もするけど気になった時が学び時

- ルーティングに関するレールが敷かれた形になるのは明確にメリット
- 命名規則を覚えるのがちょっと大変か?と思わなくもないが、大した情報量ではない
- 数が増えてきたときに見やすさが勝ちそうだ
- pathのparamに型がついたり、validationが掛けられるのはとても大きい
- 自分は今まで自前で判定書いてたな。便利になってる
- TanstackQueryの文脈も合わせて、Suspence前提で、suspendedな状態とError状態のComponentをそれぞれ定義するのが一般的?っぽい
- TanstackQueryいるか?って思ったけどTanstackRouterはreadのみで更新系はないらしい。ルーターだしね。そらそうか

TanstackQueryをいれた。
動作確認のために以下のようなmockを作る
export type Vocabulary = {
id: number;
word: string;
definition: string;
};
export function fetchVocablaries(): Promise<Vocabulary[]> {
return new Promise((resolve, reject) =>
setTimeout(() => {
if (Math.random() < 0.3) {
reject(new Error("Failed to fetch vocablaries"));
return;
}
const vocablary1: Vocabulary = {
id: 1,
word: "vocabulary",
definition: "the body of words used in a particular language.",
};
const vocablary2: Vocabulary = {
id: 2,
word: "sehr",
definition: "very (adverb)",
};
resolve([vocablary1, vocablary2]);
}, 2000)
);
}

useSuspenseQueryを使うと、isLoadingで分岐するような責務を親に渡せるらしい
import { useSuspenseQuery } from "@tanstack/react-query";
import { fetchVocablaries } from "./api";
export const Component = () => {
const { data } = useSuspenseQuery({
queryKey: ["vocablaries"],
queryFn: fetchVocablaries,
});
return (
<div>
<h1 className="text-3xl font-bold underline">Vocabulary List</h1>
<ul>
{data.map((item) => (
<li key={item.id}>{item.word}</li>
))}
</ul>
</div>
);
};
export const ErrorBoundary = () => {
return <div>Error</div>;
};
export const Loading = () => {
return <div>Loading Vocablaries...</div>;
};
export const VocabularyList = {
Component: Component,
Error: ErrorBoundary,
Loading: Loading,
};
./routes/vocablary/index.tsxはこんな感じ
import { createFileRoute } from "@tanstack/react-router";
import { VocabularyList } from "./-components/vocablary/VocabularyList";
export const Route = createFileRoute("/vocablary/")({
component: Vocabulary,
pendingComponent: VocabularyList.Loading,
errorComponent: VocabularyList.Error,
});
function Vocabulary() {
return (
<div>
<VocabularyList.Component />
</div>
);
}
動いてはいるっぽい。ただこの場合もし複数SuspenseするComponentがある場合に困りそう
Suspenceの境界===Routeの単一のpath、というように一致させるべきなのだろうか?
親子でそれぞれfetchするようだと、親のfetch>子のfetchで直列になってしまうからGraphQLのようにまとめた方が良いのかも

Suspenceで分けるの綺麗でいいなーと思ったけど、適当に置くとComponent全体が再描画されちゃうから、loaderをoverlayしたいときとかは、useSuspenseQueryじゃなくてuseQuery使っておくのがよさそうだ

mutationを学習中。特定のcacheを更新する手段だと思ってたけどあってるかな

てっきりmutationの呼び出し時にQueryKeyを設定して、mutateした後に同じQueryKeyを指定して呼び出したuseQueryで取得する値が更新されるのかと思っていたけど、どうやらそうではないっぽいな
reduxでいうdispatchがmutationで、stateがqueryKeyでcacheされる値だと思っていた。reduxベースで考えているのがそもそもよくなさそうだ
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: addVocablary,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["vocablaries"] });
},
});
更新するときはこんな感じでinvalidateQueriesを呼び出す。QueryKeyのtypoがあるとしょーもないから、定数化しておくと良さそうだ