開発準備
$ npx create-react-app <プロジェクト名> --template redux-typescript
# or
$ yarn create react-app <プロジェクト名> --template redux-typescript
formatter等の設定
PrettierとESLintを基本からまとめてみた【ReactのアプリにESLintとPrettierを導入】
必要なパッケージのインストール
① node-sass
$ yarn add node-sass
② react-hook-form
$ yarn add react-hook-form
③ material-ui
// with npm
$ npm i @mui/material @emotion/react @emotion/styled
// with yarn
$ yarn add @mui/material @emotion/react @emotion/styled
UIを構築する
① 使わないファイルを削除する
App.test.tsx
logo.svg
setupTests.ts
② App.tsxの編集する
import React from "react";
import Header from "./components/header/Header";
import styles from "./App.module.scss";
const App: React.FC = () => {
return (
<div className={styles.root}>
<div className={styles.wrapper}>
<Header />
</div>
</div>
);
};
export default App;
③ App.module.scssの作成する
④ src/components/header/Header.tsx と Header.module.scss を作成する
import React from "react";
import { AppBar, Toolbar, Typography } from "@mui/material";
import styles from "./Header.module.scss";
const Header: React.FC = () => {
return (
<div className={styles.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={styles.title}>
News
</Typography>
</Toolbar>
</AppBar>
</div>
);
};
export default Header;
⑤ App.tsx を編集する
import React from "react";
import Header from "./components/header/Header";
import styles from "./App.module.scss";
const App: React.FC = () => {
return (
<div>
<Header />
</div>
);
};
export default App;
⑥ App.module.scss を編集する
.root {
background-color: antiquewhite;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
.wrapper {
height: 100vh;
width: 100vw;
border-radius: 10px;
background-color: 3f1f2f7;
padding: 10px 40px;
}
⑦ src/components/Header.tsx を編集する
import React from "react";
import { AppBar, Toolbar, Typography } from "@mui/material";
import styles from "./Header.module.scss";
const Header: React.FC = () => {
return (
<div className={styles.root}>
<AppBar position="static" className={styles.app_bar}>
<Toolbar className={styles.tool_bar}>
<Typography variant="h6" className={styles.title}>
Redux Toolkit Todo
</Typography>
</Toolbar>
</AppBar>
</div>
);
};
export default Header;
⑧ src/components/Header.module.scss を編集する
import React from "react";
import { AppBar, Toolbar, Typography } from "@mui/material";
import styles from "./Header.module.scss";
const Header: React.FC = () => {
return (
<div className={styles.root}>
<AppBar position="static" className={styles.app_bar}>
<Toolbar className={styles.tool_bar}>
<Typography variant="h6" className={styles.title}>
Redux Toolkit Todo
</Typography>
</Toolbar>
</AppBar>
</div>
);
};
export default Header;
TaskFormコンポーネント作成
①features/task/taskForm/TaskForm.tsxとTaskForm.module.scssを作成する
import React from "react";
import styles from "./TaskForm.module.scss";
import { TextField } from "@mui/material";
const TaskForm: React.FC = () => {
return (
<div className={styles.root}>
<form className={styles.form} noValidate autoComplete="off">
<TextField
id="outlined-basic"
label="New Task"
variant="outlined"
className={styles.text_field}
/>
</form>
</div>
);
};
export default TaskForm;
.root {
width: 30vw;
margin-bottom: 20px;
.form {
.text_field {
width: 100%;
}
}
}
react-hook-formの導入
import React from "react";
import { useForm } from "react-hook-form";
import styles from "./TaskForm.module.scss";
import { TextField } from "@mui/material";
type Inputs = {
taskTitle: string;
};
const TaskForm: React.FC = () => {
const { register, handleSubmit, reset } = useForm();
const handleCreate = (data: Inputs) => {
console.log(data);
reset();
};
return (
<div className={styles.root}>
<form onSubmit={handleSubmit(handleCreate)} className={styles.form}>
<TextField
id="outlined-basic"
label="New Task"
variant="outlined"
inputRef={register}
name="taskTitle"
className={styles.text_field}
/>
</form>
</div>
);
};
export default TaskForm;
TaskItemコンポーネント作成
①features/task/taskItem/TaskItem.tsxとTaskItem.module.scssを作成する
import React from "react";
import { Checkbox } from "@mui/material";
import { EventNoteIcon, EditIcon, DeleteIcon } from "@mui/icons-material";
import styles from "./TaskItem.module.scss";
interface PropTypes {
task: { id: number; title: string; completed: boolean };
}
const TaskItem: React.FC<PropTypes> = ({ task }) => {
return (
<div className={styles.root}>
<div className={styles.title}>
<EventNoteIcon />
<div className={styles.title_text}>{task.title}</div>
</div>
<div className={styles.right_item}>
<Checkbox
checked={task.completed}
onClick={() => console.log(`check${task.id}`)}
inputProps={{ "aria-label": "primary Checkbox" }}
className={styles.checkbox}
/>
<button
onClick={() => console.log(`edit${task.id}`)}
className={styles.edit_button}
>
<EditIcon className={styles.icon} />
</button>
<button
onClick={() => console.log(`delete${task.id}`)}
className={styles.delete_button}
>
<DeleteIcon className={styles.icon} />
</button>
</div>
</div>
);
};
export default TaskItem;
.wrapper {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 5px;
background-color: #ffffff;
padding: 10px 20px;
margin-bottom: 10px;
.title {
display: flex;
align-items: center;
.title_text {
margin-left: 10px;
}
}
.right_item {
display: flex;
align-items: center;
.edit_button, .delete_button {
border: none;
border-radius: 5px;
background-color: #f1f2f7;
cursor: pointer;
.icon {
width: 20px;
color: #282828;
}
}
.edit_button {
margin-right: 5px;
}
}
}
.modal {
display: flex;
justify-content: center;
align-items: center;
.modal_content {
background: #FFFFFF;
width: 40vw;
height: 40vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 5px;
&:focus {
outline: none;
}
.modal_title {
margin-bottom: 20px;
font-size: 20px;
}
}
}
TaskListコンポーネント作成
①features/task/taskTaskList/TaskList.tsxとTaskList.module.scssを作成する
import React from "react";
import styles from "./TaskList.module.scss";
import sampleData from "./sampleData.json";
import TaskItem from "../taskItem/TaskItem";
const TaskList: React.FC = () => {
return (
<div className={styles.root}>
{sampleData.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</div>
);
};
export default TaskList;
.wrapper {
height: 49vh;
overflow: hidden;
overflow-y: auto;
}
Redux側の実装
①features/task/taskSlice.tsを作成し、
features/counter/coutenrSlice.tsのソースコードをコピペし、編集する。
②actionをexportして、selectTaskの定義する。
③createSliceを用いてsliceの作成する。(name,initialState,reducers)
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState, AppThunk } from "../../app/store";
//stateの型を記述する
export interface TaskState {
//taskが何個あるのかを管理する
idCount: number;
//storeに保存するtaskの一覧
tasks: { id: number; title: string; completed: boolean }[];
//taskのtitleを編集する際にどのtaskが選択されているか
selectedTasks: { id: number; title: string; completed: boolean };
//Modalを開くか閉じるかのフラグ
isModalOpen: boolean;
}
//stateの初期値を記述する
const initialState: TaskState = {
idCount: 1,
//taskが何個あるのかを管理する
tasks: [{ id: 1, title: "Task A", completed: false }],
//storeに保存するtaskの一覧
selectedTasks: { id: 0, title: "", completed: false },
//Modalを開くか閉じるかのフラグ
isModalOpen: false,
};
//name,initialState,reducersを作成
export const taskSlice = createSlice({
name: "task",
initialState,
reducers: {
//createReducersのtaskの作成
createTask: (state, action) => {
//stateのidCoutを1増やす
state.idCount++;
const newTask = {
id: state.idCount,
title: action.payload,
completed: false,
};
//state.tasksを更新する
state.tasks = [newTask, ...state.tasks];
},
},
});
export const { createTask } = taskSlice.actions;
//selectedTaskの定義
export const selectedTask = (state: RootState): TaskState["tasks"] =>
state.task.tasks;
export default taskSlice.reducer;
④app/store.tsを編集する。
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import taskReducer from "../features/task/taskSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
//RootStateにaskReducerが含まれる
task: taskReducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
ReactとReduxの結合
①Task取得機能を実装する。
TaskListでtask一覧をReduxから受け取る。
②sampleData.jsonを削除する。
import React from "react";
//react reduxからimportを受ける
import { useSelector } from "react-redux";
//selectTaskをtaskSliceからimport
import { selectTask } from "../taskSlice";
import styles from "./TaskList.module.scss";
//import sampleData from "./sampleData.json";
import TaskItem from "../taskItem/TaskItem";
const TaskList: React.FC = () => {
//"task"stateをtaskSliceから取得
const tasks = useSelector(selectTask);
return (
<div className={styles.root}>
{/* //mapで展開 */}
{tasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</div>
);
};
export default TaskList;
Task追加機能を実装
①TaskFormでcreateTaskをimportする。
import React from "react";
import { useForm } from "react-hook-form";
import styles from "./TaskForm.module.scss";
//useDispatchをreact-reduxからimportする
import { useDispatch } from "react-redux";
import { TextField } from "@mui/material";
//TaskFormでcreateTaskをimportする
import { createTask } from "../taskSlice";
type Inputs = {
taskTitle: string;
};
const TaskForm: React.FC = () => {
//dispatchを定義する
const dispatch = useDispatch();
const { register, handleSubmit, reset } = useForm();
const handleCreate = (data: Inputs) => {
//dispatch関数を用いて、createTaskを発火させる //createTaskの引数として、data.taskTitleを渡す
dispatch(createTask(data.taskTitle));
reset();
};
return (
<div className={styles.root}>
<form onSubmit={handleSubmit(handleCreate)} className={styles.form}>
<TextField
id="outlined-basic"
label="New Task"
variant="outlined"
inputRef={register}
name="taskTitle"
className={styles.text_field}
/>
</form>
</div>
);
};
export default TaskForm;
参考サイト
【ReduxToolkit入門】#4-1 ~簡易TODOアプリ: Redux Toolkitの理解~
【ReduxToolkit入門】#4-2 ~簡易TODOアプリ: 開発準備編~
【ReduxToolkit入門】#4-3 ~簡易TODOアプリ: UI構築前編~
【ReduxToolkit入門】#4-4 ~簡易TODOアプリ: UI構築後編~
【ReduxToolkit入門】#4-5 ~簡易TODOアプリ: Redux実装前編~
【ReduxToolkit入門】#4-6 ~簡易TODOアプリ: Redux実装中編-1~
