30分で習うより慣れる! React + Redux Toolkit + TypeScript の Todoアプリチュートリアル その1

# はじめに
最近 React を勉強し始めた新参者です。
:older_man:「とりあえず TypeScript で始めよう → Redux って便利そう → Redux Toolkit ってのがあるのか~ ・・・・・・・・・ :angel: 」
という具合にインターネットを潜ってチュートリアル始めたら、情報が多かったり古かったりEnglishだったりでカオスったので、習うより慣れるチュートリアルをここに残します。
習うより慣れたい皆様の参考になれば幸いです。

## :computer: 環境

```zsh
% sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7 //Catalinaさん、そろそろ上げないと、、、
BuildVersion:	19H2

% brew -v
Homebrew 3.0.11

% nodebrew -v
nodebrew 1.0.1

% node -v
v14.16.1

% yarn -v
1.22.10
```

# :trumpet: Start!! 



## プロジェクト作成

Redux+TypeScript テンプレートを使用

:point_down: こちら

https://github.com/reduxjs/cra-template-redux-typescript

```zsh
yarn create react-app redux-sample --template redux-typescript
```

```zsh:作成したプロジェクトへ移動
cd redux-sample
```

テンプレートに含まれてるRedux関連のモジュール達です

```zsh:
% ls -d -e node_modules/*redux*
node_modules/@reduxjs		node_modules/react-redux	node_modules/redux		node_modules/redux-thunk
```

Redux を TypeScript に対応させるやつをインストールします (雑 

```zsh
yarn add typescript-fsa typescript-fsa-reducers
```

以下の構成が出来上がります
ディレクトリ構成

```zsh:~/redux-sample
% tree -I ".DS_Store|node_modules|.git"  -La 4 --dirsfirst
.
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── app
│   │   ├── hooks.ts
│   │   └── store.ts
│   ├── features
│   │   └── counter
│   │       ├── Counter.module.css
│   │       ├── Counter.tsx
│   │       ├── counterAPI.ts
│   │       ├── counterSlice.spec.ts
│   │       └── counterSlice.ts
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
├── .gitignore
├── README.md
├── package.json
├── tsconfig.json
└── yarn.lock
```

これを以下に変更します (こちらはお好みでどうぞ)
新ディレクトリ構成

```zsh
.
├── public
│   └── # 省略
├── src
│   ├── app
│   │   ├── hooks.ts
│   │   └── store.ts
│   ├── features
│   │   └── counter
│   │       ├── counterAPI.ts
│   │       ├── counterSlice.spec.ts
│   │       └── counterSlice.ts
│   ├── pages # <-- 作成
│   │   ├── assets # <-- 作成
│   │   │   └── logo.svg # <-- 移動
│   │   ├── counter # <-- 作成
│   │   │   ├── Counter.module.css # <-- 移動
│   │   │   └── Counter.tsx # <-- 移動
│   │   ├── App.css # <-- 移動
│   │   ├── App.test.tsx # <-- 移動
│   │   └── App.tsx # <-- 移動
│   ├── index.css
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
└── # 省略
```

既存のソースをちょっと修正する

```typescript:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './pages/App'; // <-- 修正
// 以下省略
```

```typescript:src/pages/App.tsx
import React from 'react';
import logo from './assets/logo.svg'; // <-- 修正
import { Counter } from './counter/Counter'; // <-- 修正
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {/* --- ここから削除 ---
        <Counter />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        --- ここまで --- */}
        <span>
// 以下省略
```

```typescript:src/pages/counter/Counter.tsx
import React, { useState } from 'react';
// 省略
} from '../../features/counter/counterSlice'; // <-- 修正
import styles from './Counter.module.css';

export function Counter() {
  //省略

  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      <div className={styles.row}>
        // 省略
      </div>
      {/*  --- ここから追記 --- */}
      <p>
        Edit <code>src/pages/counter/Counter.tsx</code> and save to reload.
      </p>
      {/*  --- ここまで ---  */}
    </div>
  );
}
```

## TodoアプリのUI作成

Todoアプリ用のページディレクトリを作成

```zsh
mkdir src/pages/todo
```

Todoアプリのベースを作成

```typescript:src/pages/todo/TodoApp.tsx
import React from 'react';
import AddTodo from './components/AddTodo'

function TodoApp() {
  return (
    <div>
      <AddTodo />
      <p>
        Edit <code>src/pages/todo/TodoApp.tsx</code> and save to reload.
      </p>
    </div>
  );
};

export default TodoApp;
```

Todo 入力用の Input と Button を作成

```typescript:src/pages/todo/components/AddTodo.tsx
import React from "react";

export default function AddTodo(): JSX.Element {
  const [text, setText] = React.useState("");

  function handleChange(e: { target: HTMLInputElement }) {
    setText(e.target.value);
  }

  function handleSubmit(e: any) {
    e.preventDefault();

    if (!text.trim()) {
      return;
    }

    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={handleChange} />
      <button type="submit">Add Todo</button>
    </form>
  );
}
```

### ルーティングの実装

ルーティングモジュールをインストール

```zsh
yarn add react-router-dom  @types/react-router-dom
```

App.tsx から各ページを URL 毎に読み込むように修正

```typescript:src/pages/App.tsx
import React from 'react';
import logo from './assets/logo.svg';
import { BrowserRouter as Router, Route } from 'react-router-dom'; // <-- 追記
import { Counter } from './counter/Counter';
import Todo from './todo/TodoApp'; // <-- 追記
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        // --- ここから追記 ---
        <Router>
          <Route exact path="/" component={Counter} />
          <Route exact path="/todo" component={Todo} />
        </Router>
        // --- ここまで ---
        // 省略
      </header>
    </div>
  );
}

export default App;
```

ロゴの主張が強いので以下の css を修正

```css:src/App.css
.App-logo {
  height: 20vmin;    /* 40 --> 20 */
  /* 省略 */
}

.App-header {
  min-height: 10vh; /* 100 --> 10 */
  /* 省略 */
}
```

### 起動して見た目を確認

```zsh
yarn run start
```

:point_down: ```localhost:3000/``` にアクセスしたときの見た目 (サンプルソース)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112011/6d848b4c-c304-0e74-1a9f-5e908e93098a.png)

:point_down: ```localhost:3000/todo``` にアクセスしたときの見た目

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112011/32ccd8a2-53c0-4df5-f03d-646459ab5dfe.png)

## Todoを追加できるようにする

### Todoリストの componet を作成

-  Todoリスト

```typescript:src/pages/todo/components/TodoList.tsx
import * as React from "react";
import TodoItem from './TodoItem'

export default function TodoList() {
  const text: any = ""
  return (
    <ul>
      <TodoItem {...text} />
    </ul>
  );
}
```

-  Todo単体

```typescript:src/pages/todo/components/TodoItem.tsx
import * as React from "react";

interface TodoProps {
  text: string;
}

export default function TodoItem( { text }: TodoProps ) {
  return (
    <li>
      {text}
    </li>
  );
}
```

- 作成した Todo Components を読み込む

```typescript:src/pages/todo/TodoApp.tsx
import React from 'react';
import AddTodo from './components/AddTodo'
import TodoList from './components/TodoList' // <-- 追記

export default function TodoApp() {
  return (
    <div>
      <AddTodo />
      <TodoList /> {/* <-- 追記 */}
      <p>
        Edit <code>src/pages/todo/TodoApp.tsx</code> and save to reload.
      </p>
    </div>
  );
};
```

### 起動して見た目を確認

:point_down: Todoリストの表示部分ができました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112011/9b72e7d5-60c1-6c3d-5216-866af37cec7b.png)


## 入力した値をTodoリストに出力する

### Slice を作成

```typescript:src/features/todo/todoSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, AppDispatch, RootState } from "../../app/store";
import { Todo } from './types'

const initialState: Todo[] = [];

const todoSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTodo(state, action: PayloadAction<Todo>) {
      state.push(action.payload);
    },
  }
});

export const addTodo = (text: string): AppThunk => async (
  dispatch: AppDispatch
) => {
  const newTodo: Todo = {
    id: Math.random().toString(36).substr(2, 9),
    text: text
  };
  dispatch(todoSlice.actions.addTodo(newTodo));
};

export default todoSlice.reducer;
```

- Todo Interface type を作成

```typescript:src/features/todo/types.ts
export interface Todo {
  id: string;
  text: string;
}
```

### Reducer を作成

```typescript:src/app/rootReducer.ts
import { combineReducers } from "@reduxjs/toolkit";
import counter from "../features/counter/counterSlice";
import todos from "../features/todo/todoSlice";

const rootReducer = combineReducers({
  counter,
  todos
});

export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;
``` 

### Store を修正

```typescript:src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import rootReducer from "./rootReducer";

export const store = configureStore({
  reducer: rootReducer
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;
```

### 各 Todo Components を修正して結合する

```typescript:src/pages/todo/components/AddTodo.tsx
import React from "react";
import { useDispatch } from "react-redux"; // <-- 追記
import { addTodo } from "../../../features/todo/todoSlice"; // <-- 追記

export default function AddTodo(): JSX.Element {
  const dispatch = useDispatch(); // <-- 追記
  const [text, setText] = React.useState("");

  function handleChange(e: { target: HTMLInputElement }) {
    setText(e.target.value);
  }

  function handleSubmit(e: any) {
    e.preventDefault();

    if (!text.trim()) {
      return;
    }
    dispatch(addTodo(text)); // <-- 追記

    setText("");
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={handleChange} />
      <button type="submit">Add Todo</button>
    </form>
  );
}

```

```typescript:src/pages/todo/components/TodoList.tsx
import * as React from "react";
import { useSelector } from "react-redux"; // <-- 追記
import { RootState } from "../../../app/rootReducer" // <-- 追記
import { Todo } from "../../../features/todo/types"; // <-- 追記
import TodoItem from './TodoItem'

//export default function TodoList() { <-- 削除
  // const text: any = ""; <-- 削除
  
export default function TodoList() { // <-- 追記
  const todos: Todo[] = useSelector((state: RootState) => // <-- 追記
    state.todos // <-- 追記
  ); // <-- 追記
  return (
    <ul>
      {/* --- 追記 ---  */}
      {(todos || []).map(todo => (
        <TodoItem {...todo} />
      ))}
      {/* --- ここまで --- */}
      {/* <TodoItem {...text} /> <-- 削除 */}
    </ul>
  );
}
```
### 起動して動作確認

![qiita_react.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112011/fd70bb3c-ade3-4132-bddc-6f201f0dbc3c.gif)


# 最後に
いかがでしたでしょうか、慣れましたか？
こちらのサイトがとても分かりやすく解説してくれています。感謝(-人-)
おそらく、この後にこちらをみると理解が深まると思います。

https://www.hypertextcandy.com/learn-react-redux-with-hooks-and-redux-starter-kit

皆様の助けになると幸いです。
チュートリアル通りだと、その2以降は、タスクを完了させたり、タスクの完了・未完了のフィルタリングですが、そのうち投稿したいとおもうのですが、もう皆さんなら出来ると思うのです。
断じて力尽きたわけではありません。私もこれから勉強していきます(＞＜)

今回のソースはこちらに置いておきます

https://github.com/ytamayose/redux-todo-sample

でわ :wave: