react shadcn(hook form zod SWR) でのフォームとfetchの実装の備忘録
はじめに
こんにちは.
T.Miuraです.
みなさんいかがお過ごしでしょうか?
私は元気にやっております.
モダンフロントのお仕事がない弊社ですが文句を言っていても仕方がないの個人でReactを触っています.そろそろ本格的にNext.jsに入りますヨ
(シンプルに趣味なだけなんですが...)
本ブログではここ最近学んだことの備忘録です.
フロント初めて大体2ヶ月程度でアホなこと言っている姿を笑って下さい.
興奮してきたな
題材としてpokeAPIを使用したfetchです
詳しいコンポーネント分けはしていません!!!!!!
動く完成品です
環境構築
技術スタック
Docker
vite
React
shadcn
react-hook-form
zod
SWR
Dockerfile
FROM node:20-alpine3.20
WORKDIR /usr/src/app
RUN apk update && npm update && apk add \
git \
vim \
tree \
curl \
wget
docker-compose.yml
services:
node:
build: .
environment:
- NODE_ENV=development
volumes:
- ./:/usr/src/app
ports:
- '3000:3000'
tty: true
コンテナの世界へ入門
docker compose up -d
docker compose exec node sh
viteのプロジェクト作成
npm create vite@latest
react-tscを選択
cd ~作成したプロジェクトへ~
npm install
ライブラリのインストール
tailwindcssのインストール
npm install @tailwindcss/vite
app.cssにtailwindcssをインポート
@import "tailwindcss";
shadcnのインストール
npx shadcn@latest init
npm install -D @types/node
npx shadcn@latest add form
swrのインストール
npm install swr
react-hook-formのインストール
npm install react-hook-form
初期設定
私はここでvite.config.tsを編集しました.
詳しくはshadcnのインストールガイドを参照してください.
docker-compose.ymlでポートを3000にしているのでvite.config.tsでも3000にします.
import path from "path"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react-swc"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
//ここ追加することでdocker コンテナからlocalhost:3000でアクセスできるようになります
server: {
port: 3000,
host: '0.0.0.0',
},
})
tsconfig.jsonの設定
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
//ここから追加
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
//ここまで追加
}
tsconfig.app.jsonの設定
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
//ここから追加
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
//ここまで追加
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
さぁ奇跡の瞬間
npm run dev
React icon が見えれば成功ですね
フォーム実装
それでは実装していきます
まずはzodから仕留めます
zod でバリデーションスキーマの作成
import { z } from "zod";
export const formSchema = z.object({
pokeId: z.string().regex(/^(?:[1-9][0-9]{0,2}|10[0-1][0-9]|102[0-5])$/, { message: "1から1025までの数字を入力してください" }),
});
そして次はuseFormのカスタムフックを作成してやります.
先ほど作成したzodのスキーマをreact-hook-formにresolverとして渡します
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { formSchema } from "./validation";
const useFormWithValidation = () => {
const form = useForm<z.infer<typeof formSchema>>({
//ここのmodeでいつバリデーションを行うかを指定しますよ
//onChangeでは入力した度にバリデーションを行うようになりますよ
//入力のたびに赤文字でると興奮しちゃうので今回はonSubmitで行きましょい
mode: "onSubmit",
resolver: zodResolver(formSchema),
defaultValues: {
pokeId: "",
},
});
return form;
}
export default useFormWithValidation;
この段階でフォーム大枠が完成しましたね.
次はshadcnのコンポーネントを使用してフォームを作成します.
import "./App.css";
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import type{ SubmitHandler } from "react-hook-form";
import useFormWithValidation from "./useFormWithValidation";
type formInputs = {
pokeId: string;
};
function App() {
const form = useFormWithValidation();
const onSubmit: SubmitHandler<formInputs> = (data) => {
console.log(data);
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="pokeId"
render={({ field }) => (
<FormItem>
<FormLabel>ポケモンのID</FormLabel>
<FormControl>
<Input placeholder="ポケモンのIDを入力してください" {...field} />
</FormControl>
<FormDescription>
ポケモンのIDを入力してください
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">送信</Button>
</form>
</Form>
</>
);
}
export default App;
データフェッチング
お次はuseSWRの番です覚悟しやがれ
まずはuseSWRを使用したカスタムフックを作成します
import useSWR from "swr";
export type Poke = {
id: number;
name: string;
image: string;
type: string[];
height: number;
weight: number;
abilities: string[];
};
// PokeAPIのレスポンス型を定義
type PokeAPIResponse = {
id: number;
name: string;
sprites: {
front_default: string;
};
types: { type: { name: string } }[];
height: number;
weight: number;
abilities: { ability: { name: string } }[];
};
export const useFetchPoke = (id: string) => {
const fetcher = (url: string) => fetch(url).then((res) => res.json());
const { data, error, isLoading } = useSWR(
`https://pokeapi.co/api/v2/pokemon/${id}`,
fetcher
);
const mapToPoke = (data: PokeAPIResponse): Poke => ({
id: data.id,
name: data.name,
image: data.sprites?.front_default ?? "",
type: data.types?.map((t) => t.type.name) ?? [],
height: data.height,
weight: data.weight,
abilities: data.abilities?.map((a) => a.ability.name) ?? [],
});
return {
poke: data ? mapToPoke(data) : null,
isError: error,
isLoading
};
};
そしてそれを作成したフォームで読み込ませればあら不思議
import "./App.css";
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import type{ SubmitHandler } from "react-hook-form";
import { useState } from "react";
import useFormWithValidation from "./useFormWithValidation";
import { useFetchPoke } from "./useFetchPoke";
type formInputs = {
pokeId: string;
};
function App() {
const [pokeId, setPokeId] = useState<string>("");
const form = useFormWithValidation();
const { poke, isError, isLoading } = useFetchPoke(pokeId);
const onSubmit: SubmitHandler<formInputs> = (data) => {
setPokeId(data.pokeId);
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="pokeId"
render={({ field }) => (
<FormItem>
<FormLabel>ポケモンのID</FormLabel>
<FormControl>
<Input placeholder="ポケモンのIDを入力してください" {...field} />
</FormControl>
<FormDescription>
ポケモンのIDを入力してください
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">送信</Button>
</form>
</Form>
<p>{poke?.name}</p>
</>
);
}
export default App;
pタグの欄にポケモンの名前が表示されていますね~~~!!!!!!
UI実装とデータ表示
useSWRのisError,isLoadingを使用してエラーとローディング状態を管理してやります
import "./App.css";
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import type{ SubmitHandler } from "react-hook-form";
import { useState } from "react";
import useFormWithValidation from "./useFormWithValidation";
import { useFetchPoke } from "./useFetchPoke";
type formInputs = {
pokeId: string;
};
function App() {
const [pokeId, setPokeId] = useState<string>("");
const form = useFormWithValidation();
const { poke, isError, isLoading } = useFetchPoke(pokeId);
const onSubmit: SubmitHandler<formInputs> = (data) => {
setPokeId(data.pokeId);
};
if (isError) return <div>エラーが発生しました</div>;
if (isLoading) return <div>ローディング中...</div>;
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="pokeId"
render={({ field }) => (
<FormItem>
<FormLabel>ポケモンのID</FormLabel>
<FormControl>
<Input placeholder="ポケモンのIDを入力してください" {...field} />
</FormControl>
<FormDescription>
ポケモンのIDを入力してください
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">送信</Button>
</form>
</Form>
<p>{poke?.name}</p>
</>
);
}
export default App;
簡単ですね〜〜〜〜〜!
useSWRを使用するとキャッシュが効くのでパフォーマンスが良いらしいです!!!検証はしていません!
最後に作成したApp.tsxです
import "./App.css";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import type { SubmitHandler } from "react-hook-form";
import { useState } from "react";
import useFormWithValidation from "./useFormWithValidation";
import { useFetchPoke } from "./useFetchPoke";
type formInputs = {
pokeId: string;
};
function App() {
const [pokeId, setPokeId] = useState<string>("");
const form = useFormWithValidation();
const { poke, isError, isLoading } = useFetchPoke(pokeId);
const onSubmit: SubmitHandler<formInputs> = (data) => {
setPokeId(data.pokeId);
};
if (isError) return <div>エラーが発生しました</div>;
if (isLoading) return <div>ローディング中...</div>;
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="pokeId"
render={({ field }) => (
<FormItem>
<FormLabel>ポケモンのID</FormLabel>
<FormControl>
<Input
placeholder="ポケモンのIDを入力してください"
{...field}
/>
</FormControl>
<FormDescription>
ポケモンのIDを入力してください
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">送信</Button>
</form>
</Form>
{poke ? (
<div className="outline-solid flex flex-col items-center">
<div className="flex">
<p className="text-2xl font-bold m-2">ID:{poke?.id}</p>
<p className="text-2xl font-bold m-2">{poke?.name}</p>
</div>
<img src={poke?.image} alt={poke?.name} />
<p>タイプ:{poke?.type}</p>
<div className="flex flex-col">
<p>高さ:{poke?.height}</p>
<p>重さ:{poke?.weight}</p>
</div>
<p>~特性~</p>
{poke?.abilities.map((ability, index) => (
<span key={index}>
{ability}
<br />
</span>
))}
</div>
) : (
<p>検索しよう!</p>
)}
</>
);
}
export default App;
終わりに
ここまで読んでいただき感謝します.
色々課題は残りますね.
useCallback使った方いいんじゃ?とか
idをstringじゃなくnumberにした方がいいのでは?とか
今後も学び続けますよ
それにしてもshadcnのおかげで綺麗なUIになりますよね
CSS苦手な私にとって嬉しい存在です.
Discussion