🦔
TanStack Routerを使用してフォームを作ってみた
はじめに
TanStackは、React をはじめとする JavaScript フレームワークでよく使われる高品質なライブラリ群の集合体です。
Create React Appが非推奨であるため、個人学習でTanStack Routerをインストールして使用してみました。
今回紹介する TanStack Router v1 は、Search Paramをデフォルトで型安全に扱えるのが特徴。
この記事では、TanStack Routerを使って、
- 検索フォームからデータを入力
- 入力した値をクエリパラメータとして遷移先に渡す
- 遷移先でその値を取得・表示する
というシンプルなフォームを実装しました。
完成したもの
作ったのは以下のような簡単なアンケートフォームです。
- 名前、年齢、性別、コメントの4項目
- 入力内容を /result ページに遷移して表示
- 入力内容はURLクエリ(search params)で渡す
TanStack Router の useNavigate() で search を指定して遷移し、useSearch() で受け取るという構成です。
入力フォームページ | 入力後 | 結果表示ページ |
---|---|---|
![]() |
![]() |
![]() |
使用技術
- React 18+
- TanStack Router v1
- TypeScript
- MUI (Material UI)
フォーム実装コード(Form.tsx)
src/components/templates/Form.tsx
// src/components/templates/Form.tsx
const Form = () => {
const [form, setForm] = useState({
name: "",
age: "",
gender: "",
comment: "",
});
const handleChange = (
e:
| SelectChangeEvent<string>
| ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setForm({ ...form, [name]: value });
};
const navigate = useNavigate();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
navigate({
to: "/result",
search: form,
});
};
return (
<Container>
<Box
component="form"
sx={{
paddingTop: 4,
paddingBottom: 4,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
onSubmit={handleSubmit}
>
<Typography variant="h1" sx={{ color: "black", fontSize: 24 }}>
フォーム提出
</Typography>
<Box sx={{ width: "100%", mt: 2 }}>
<FormLabel
component="legend"
sx={{
textAlign: "left",
display: "inline-block",
width: "100%",
color: "black",
}}
>
名前
</FormLabel>
<TextField
name="name"
label="名前"
variant="outlined"
value={form.name}
onChange={handleChange}
fullWidth
/>
</Box>
<Box sx={{ width: "100%", mt: 2 }}>
<InputLabel id="demo-simple-select-label" sx={{ color: "black" }}>
年齢
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
name="age"
label="年齢"
value={form.age}
onChange={handleChange}
fullWidth
>
<MenuItem value="10代">10代</MenuItem>
<MenuItem value="20代">20代</MenuItem>
<MenuItem value="30代">30代</MenuItem>
</Select>
</Box>
<Box sx={{ width: "100%", mt: 2 }}>
<FormLabel
component="legend"
sx={{
textAlign: "left",
display: "inline-block",
width: "100%",
color: "black",
}}
>
性別
</FormLabel>
<RadioGroup
row
name="gender"
value={form.gender}
onChange={handleChange}
>
<FormControlLabel
sx={{ color: "black" }}
value="女性"
control={<Radio />}
label="女性"
/>
<FormControlLabel
sx={{ color: "black" }}
value="男性"
control={<Radio />}
label="男性"
/>
<FormControlLabel
sx={{ color: "black" }}
value="その他"
control={<Radio />}
label="その他"
/>
</RadioGroup>
</Box>
<TextField
name="comment"
label="コメント"
placeholder="コメントを入力してください"
multiline
rows={4}
value={form.comment}
onChange={handleChange}
fullWidth
margin="normal"
/>
<Button type="submit" variant="contained" color="primary" fullWidth>
送信
</Button>
</Box>
</Container>
);
};
export default Form;
結果表示ページ(Result.tsx)
src/components/templates/Result.tsx
const Result = () => {
const search = useSearch({ from: "/result" });
return (
<Container>
<Box
sx={{
paddingTop: 4,
paddingBottom: 4,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography variant="h1" sx={{ color: "black", fontSize: 24 }}>
検索結果
</Typography>
<Box sx={{ width: "100%", mt: 2, color: "black" }}>
名前: {search.name}
</Box>
<Box sx={{ width: "100%", mt: 2, color: "black" }}>
年齢: {search.age}
</Box>
<Box sx={{ width: "100%", mt: 2, color: "black" }}>
性別: {search.gender}
</Box>
<Box sx={{ width: "100%", mt: 2, color: "black" }}>
コメント: {search.comment}
</Box>
</Box>
</Container>
);
};
export default Result;
型安全を担保する validateSearch
src/routes/result.tsx
type SearchParams = {
name: string;
age: string;
gender: string;
comment: string;
};
export const Route = createFileRoute("/result")({
validateSearch: (search: Record<string, unknown>): SearchParams => {
return {
name: String(search.name ?? ""),
age: String(search.age ?? ""),
gender: String(search.gender ?? ""),
comment: String(search.comment ?? ""),
};
},
component: ResultComponent,
});
function ResultComponent() {
return (
<div className="bg-white">
<Result />
</div>
);
}
型安全にSearch Paramを管理する点については下記の記事を参考にしました。
これにより、useSearch() を使うだけで search.name などの型が推論され、補完も効きます。
他ライブラリとの違い
ライブラリ | Search Paramsの型安全 | 状態管理 | 学習コスト |
---|---|---|---|
TanStack Router | ✅ validateSearch により型安全 |
状態管理と密結合せず使える | 中程度 |
React Router | ❌ 自前で型ガード or Zod等を使う必要あり |
useSearchParams() は文字列扱い |
低 |
SWR | ❌ クエリには型安全なし(主にfetch用) | fetch時のキャッシュが強み | 中 |
ポイント:
TanStack Router はルーティングと search param の管理が一体となっており、そのまま型安全に扱える点が強力です。
つまずいたこと
-
useSearch()
を使うには、事前にvalidateSearch()
を定義しておかないと型エラーになる - RadioやSelectなど、Formのname属性の設定を忘れると値が更新されない
良かったところ
- validateSearch() による search paramsの型安全性
- フォーム送信後に navigate() で状態遷移+URL更新がスムーズ
- ルーティングと状態管理が統一されていて読みやすい
改善点・注意点
- ドキュメントがまだ少なめ(React Routerより情報が少ない)
- 導入コストがやや高め(TypeScriptに慣れていないと辛い)
- SWRやReact Queryとの役割分担が最初はやや混乱しやすい
まとめ
TanStack Routerは、フォームや検索機能など「URLと状態が強く結びつくUI」に非常に向いていると感じました。
型安全でURL連動型の開発がしたい場合、React Routerよりも一歩進んだ体験ができます。今後はさらに他の機能も使用し、理解を深めたいと思います。
Discussion