Zenn
Closed14

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

LTKSKLTKSK

普段はReactを使って開発をしているが、ライブラリなどはそこまで新しい感じではない
なので今から新規に作るとどうなるのかという作業のログを残す

どんなアプリにするかだが、ドイツ語を学習しているのでとりあえず単語を永続化できて、それをランダムに表示するようなものを目指す
技術スタックで確定しているのは以下

  • React
  • Vite
  • tailwindcss
    あとは必要になったら都度考える。見切り発車
LTKSKLTKSK

ディレクトリ構造で悩んだけど、今回はそこをやりこみたいわけではないので、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`ディレクトリは、コンポーネント固有のスタイルを整理し、グローバルスタイルから分離します。

LTKSKLTKSK

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

LTKSKLTKSK

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

LTKSKLTKSK

どっちも触ると重そうだったので、いったん切り上げてTanstackRouterを触る
installは簡単。今回はFileBasedRoutingで作る。Next.jsとかもディレクトリ構造とroutingをマッピングしているからこの機会に触れておこう
https://tanstack.com/router/latest/docs/framework/react/quick-start#srcmaintsx

  • routeの型情報がrouteTree.gen.ts に書き出される。viteの場合はplugin経由で自動更新されるように見える
  • outletになじみがなかったけど、ReactRouterにもある概念らしい
    • nestしたpathに対応する機能。/fooにOutletを置いて子Componentのroutingを指定していると、/foo/barの時は/fooに対応するComponentを表示、/foo/bazの時は/bazに対応するComponentを表示することができる。ページ内にtabとかで子ページがあるときに便利そうだ
LTKSKLTKSK

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

TanstackQueryをいれた。
https://tanstack.com/query/latest/docs/framework/react/quick-start

動作確認のために以下のような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)
  );
}
LTKSKLTKSK

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のようにまとめた方が良いのかも

LTKSKLTKSK

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

LTKSKLTKSK

てっきり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があるとしょーもないから、定数化しておくと良さそうだ

このスクラップは2ヶ月前にクローズされました
ログインするとコメントできます