React (TypeScript) + Redux Toolkit を使った、シンプルなフォームサンプルを作ります。
例として「ユーザー情報フォーム(名前とメールアドレス)」をReduxで管理します。
ディレクトリ構成
src/
├─ app/
│ └─ store.ts
├─ features/
│ └─ user/
│ ├─ userSlice.ts
│ └─ UserForm.tsx
├─ App.tsx
└─ main.tsx
1. セットアップ
# プロジェクト作成
npx create-react-app redux-form-sample --template typescript
# Redux Toolkit と React-Redux を追加
npm install @reduxjs/toolkit react-redux
2.Redux ストア設定 (src/app/store.ts)
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "@/features/user/userSlice";
export const store = configureStore({
reducer:{
user:userReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
TypeScript 5.x 以降の新しい挙動で、
"verbatimModuleSyntax": true がtsconfig.jsonに設定されていると、
「型はimport typeでインポートしなければならない」ルールが強制されます。
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
✅ ポイント
〇TypedUseSelectorHook は「型」なのでimport typeで読み込む必要があります。
〇useDispatch やuseSelectorは「値(関数)」なので、通常の import です。
3. Slice作成 (src/features/user/userSlice.ts)
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
interface UserState {
name: string;
email: string;
}
const initialState: UserState = {
name: "",
email: "",
};
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setName: (state, action: PayloadAction<string>) => {
state.name = action.payload;
},
setEmail: (state, action: PayloadAction<string>) => {
state.email = action.payload;
},
resetUser: (state) => {
state.name = "";
state.email = "";
},
},
});
export const { setName, setEmail, resetUser } = userSlice.actions;
export default userSlice.reducer;
- フォームコンポーネント (
src/features/user/UserForm.tsx)
import React from "react";
import { useAppSelector, useAppDispatch } from "@/app/hooks"; // 型付きhooks
import { setName, setEmail, resetUser } from "./userSlice";
export const UserForm: React.FC = () => {
const dispatch = useAppDispatch();
const { name, email } = useAppSelector((state) => state.user); // ← OK: 型は自動で RootState として推論される
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(`送信しました:\n名前: ${name}\nメール: ${email}`);
};
return (
<form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: "2rem auto" }} className="border border-gray-500 rounded px-4 py-4">
<h2>ユーザー情報フォーム</h2>
<div style={{ marginBottom: "1rem" }}>
<label>名前:</label>
<input
type="text"
value={name}
onChange={(e) => dispatch(setName(e.target.value))}
style={{ width: "100%", padding: "0.5rem" }}
className="border border-gray-500 rounded px-4 py-4"
/>
</div>
<div style={{ marginBottom: "1rem" }}>
<label>メール:</label>
<input
type="email"
value={email}
onChange={(e) => dispatch(setEmail(e.target.value))}
style={{ width: "100%", padding: "0.5rem" }}
className="border border-gray-500 rounded px-4 py-4"
/>
</div>
<button type="submit" style={{ marginRight: "1rem" }} className="bg bg-blue-500 rounded text-white font-bold px-4 py-4 mr-4 hover:bg-blue-700 cursor-pointer">
送信
</button>
<button type="button" onClick={() => dispatch(resetUser())} className="bg bg-gray-500 rounded text-white font-bold px-4 py-4 mr-4 hover:bg-gray-700 cursor-pointer">
リセット
</button>
</form>
);
};
5. ルート設定 (src/App.tsx)
import { UserForm } from './features/user/UserForm';
const App = ()=>{
return (
<div className="flex justify-center items-center min-h-screen p-8 bg-gray-50">
<UserForm />
</div>
)
}
export default App;
6. Redux Provider設定 (src/main.tsx)
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { Provider } from 'react-redux';
import { store } from './app/store.ts';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)
動作
入力した「名前」と「メール」はReduxの状態に保存される。
フォーム送信時にReduxの状態をalertで確認。
「リセット」ボタンでRedux状態を初期化。
トラブルシューティング
1.✅ ① verbatimModuleSyntax による型インポートエラー
エラー内容:
'TypedUseSelectorHook' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
これはTypeScript 5.x以降の新しい挙動で、
"verbatimModuleSyntax": true がtsconfig.jsonに設定されていると、
「型はimport typeでインポートしなければならない」ルールが強制されます。
🔧 修正方法
/src/app/hooks.ts のインポートを以下のように変更します 👇
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
✅ ポイント
〇TypedUseSelectorHook は「型」なのでimport typeで読み込む必要があります。
〇useDispatch やuseSelectorは「値(関数)」なので、通常のimportです。
✅ ② ファイル名の大文字・小文字の不一致
エラー内容:
File name '.../App/hooks.ts' differs from already included file name '.../app/hooks.ts' only in casing.
これは、import { useAppSelector, useAppDispatch } from "@/App/hooks"; の部分で、
App フォルダ名を大文字で書いているために起きています。
Windows は大文字・小文字を区別しないのですが、TypeScript は区別します。
🔧 修正方法
UserForm.tsx のインポートを以下のように直します 👇
import { useAppSelector, useAppDispatch } from "@/app/hooks";
または、もし実際に src/App/hooks.ts が存在しているなら、
app とAppのどちらかに統一してください(一般的には全部小文字にします)。
✅ ③ state.user の型が unknown 扱いされる
エラー内容:
Property 'name' does not exist on type 'unknown'
Property 'email' does not exist on type 'unknown'
これは、Redux の RootState 型がunknownと推論されている場合に起こります。
useAppSelector の型付けができていない、または store.ts のRootState定義が不完全です。
🔧 修正方法(store.ts を確認)
store.ts がこのようになっているか確認してください 👇
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "@/feature/user/userSlice";
export const store = configureStore({
reducer: {
user: userReducer,
},
});
// ✅ RootState と AppDispatch の型を明示的に定義
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
これで useAppSelector がRootState型を正しく参照でき、
state.user.name や state.user.email に型が付きます。
サイト
React-Reduxに関する記事