ざっくり React & Redux with TypeScript

# はじめに
## この記事を書いた動機
[3年前に書いた記事](https://qiita.com/pullphone/items/d28baeb296666a4847b8)に対して未だにちょこちょこと反応が来るものの、自分自身はもうこの方法ではやってないし、1年で大きく動くと言われるフロントエンド界隈で3年以上前の方法で学ばれるのもまずいと思い、知識の整理も込めて比較的新しい書き方で当該記事をリメイクしたい。

## 方針
- TypeScript を使う
- React と Redux と React-Thunk を使う部分は変えない
- `redux-starter-kit` を使って楽する

# 環境構築
## npm install
`npm init` した上で実行。
このへんに関してはあまり何も考えずに入れる。

```shell
# webpack系
npm install --save-dev webpack webpack-cli webpack-dev-server clean-webpack-plugin html-webpack-plugin

# React & Redux
npm install --save-dev react react-dom redux react-redux redux-starter-kit

# TypeScript
npm install --save-dev typescript tslint ts-loader @types/react @types/react-dom @types/react-redux
```

## webpackの設定
`src/index.tsx` をルートとして、ビルド時には `build` 以下にアセット群を吐き出すような場合は以下のように設定する。
`html-webpack-plugin` は元となるテンプレートからHTMLを生成してくれる。
`clean-webpack-plugin` はビルド生成先のディレクトリ配下にある不要なファイルを消してくれる。

```js:webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: __dirname + '/src/index.tsx',
  plugins: [
    new CleanWebpackPlugin({
      cleanAfterEveryBuildPatterns:['build']
    }),
    new HtmlWebpackPlugin({
      template: 'src/templates/index.html'
    }),
  ],
  output: {
    path: __dirname + '/build',
    filename: '[name].[contenthash].js'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [
      { test: /\.tsx?$/, loader: 'ts-loader' }
    ]
  }
}
```

## TypeScriptコンパイラの設定
この辺もあんまり考えることなく設定。

```json:tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "experimentalDecorators": true,
    "target": "es5",
    "jsx": "react",
    "lib": [
      "dom",
      "es6"
    ]
  },
  "include": [
    "src"
  ],
  "compileOnSave": false
}
```

## tslint設定
`tslint:recommend` を使った上で、コードを書いてるときに指摘されて「これはめんどいな…」と思ったものを `rules` に追加して、無効にしたりカスタマイズしていくと良さそう。

```json:tslint.json
{
  "extends": ["tslint:recommended"],
  "rules": {
    "object-literal-sort-keys": false,
    "ordered-imports": false,
    "no-console": [true, "log"]
  }
}
```

## ビルド&開発用サーバー設定
前は grunt とか使ってたりしたけど、最近はそのへんをすべて webpack に集約させているので、滅茶苦茶複雑なビルドパイプラインとかが必要でない限りは大体 `npm scripts` で事足りるようになった。
最終的な `package.json` はこんな感じになる。

```json:package.json
{
  "name": "react-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "clean": "rm -rf ./node_modules package-lock.json && npm i",
    "build": "webpack -p",
    "start": "webpack-dev-server -d --content-base ./public"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@types/react": "^16.9.1",
    "@types/react-dom": "^16.8.5",
    "@types/react-redux": "^7.1.1",
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^3.2.0",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-redux": "^7.1.0",
    "redux": "^4.0.4",
    "redux-starter-kit": "^0.6.3",
    "ts-loader": "^6.0.4",
    "tslint": "^5.18.0",
    "typescript": "^3.5.3",
    "webpack": "^4.39.2",
    "webpack-cli": "^3.3.6",
    "webpack-dev-server": "^3.8.0"
  }
}
```

## 動作確認
### テンプレート追加
```html:src/templates/index.html
<html lang="ja">
<body>
    <div id="root"></div>
</body>
</html>
```

### 文字を表示するだけの tsx を追加
```tsx:src/index.tsx
import React from "react";
import ReactDOM from "react-dom";

ReactDOM.render(
  <h1>Hello World!</h1>,
  document.getElementById("root"),
);
```

### ビルドして表示する
下記のコマンドを叩くと `webpack-dev-server` が起動するので、ブラウザで `http://localhost:8080` にアクセスして `Hello World!` が表示されれば準備完了。
コード差分を検知してホットリローディングされるので、開発する際には立ち上げっぱなしで良い。

```shell
npm start
open http://localhost:8080/
```

# ToDoアプリ実装
## ディレクトリ構成
いわゆる `ducks` スタイルというものを採用。
Redux が出始めの頃は `action` と `reducer` でディレクトリを分ける感じだったが、最近はまとめて書くやり方のほうが主流っぽい。
React Component のうち、Redux とのやりとりをする `containers` とそれ以外の `components` に分けるという構成もあるのだが、小規模な構成のうちはとりあえず全部 components に突っ込んでしまっても良いと思う。
初期状態の `src` ディレクトリ以下の構成はこういう感じ。

```
src
├── index.tsx
├── components/
├── modules/
└── templates/
```

## moduleを実装
Redux の action と reducer の部分を実装する。
前半部分に interface を定義する部分が増えたものの、全体的なコード量は以前と比べて減ったのではないだろうか？

前のやり方との違いとして、state を変更する部分で `Object.assign` などを使わなくても `redux-starter-kit` 側でいい感じにやってくれているので、直で state の値をいじっても良くなった（内部的には[Immer](https://github.com/immerjs/immer)を使ってるらしい）。

`reducers` として定義しているものを React 側からは `taskModule.actions.addTask()` という風にして呼び出す。

また、非同期処理で `redux-thunk` を使う場合は、こういう感じ（下記コードの `saveTasks` を参照）で書くと良い。

```ts:src/modules/taskModule.ts
import { createSlice, PayloadAction } from "redux-starter-kit";

export interface ITask {
  id: number;
  name: string;
  isExecuted: boolean;
}

interface IEditTask {
  id: number;
  name: string;
}

interface ILoadingState {
  isLoading: boolean;
  isSuccess: boolean;
}

export interface IState {
  addTask: IEditTask;
  editTask: IEditTask;
  tasks: ITask[];
  saveState: ILoadingState;
}

const initialState: IState = {
  addTask: {
    id: 1,
    name: "",
  },
  editTask: {
    id: -1,
    name: "",
  },
  tasks: [],
  saveState: {
    isLoading: false,
    isSuccess: false,
  },
};

const taskModule = createSlice({
  initialState,
  reducers: {
    setAddTaskName: (state: IState, action: PayloadAction<string>) => {
      state.addTask.name = action.payload;
    },
    setEditTaskById: (state: IState, action: PayloadAction<number>) => {
      const task = state.tasks.find((t) => t.id === action.payload);
      if (!task) {
        return;
      }
      state.editTask = task;
    },
    setEditTaskName: (state: IState, action: PayloadAction<string>) => {
      state.editTask.name = action.payload;
    },
    addTask: (state: IState) => {
      if (!state.addTask.name) {
        return;
      }
      state.tasks.push({
        id: state.addTask.id,
        name: state.addTask.name,
        isExecuted: false,
      });
      state.addTask.id = state.addTask.id + 1;
      state.addTask.name = "";
    },
    editTask: (state: IState) => {
      const task = state.tasks.find((t) => t.id === state.editTask.id);
      if (!task) {
        return;
      }
      task.name = state.editTask.name;
      state.editTask.id = -1;
    },
    endTask: (state: IState, action: PayloadAction<number>) => {
      const task = state.tasks.find((t) => t.id === action.payload);
      if (!task) {
        return;
      }
      task.isExecuted = true;
    },
    setSaveState: (state: IState, action: PayloadAction<ILoadingState>) => {
      state.saveState = action.payload;
    },
  },
});

export const { actions: taskActions } = taskModule;
export default taskModule;

export const saveTasks = () => {
  return async (dispatch, getState) => {
    const { task } = getState();
    if (task.saveState.isLoading) {
      return;
    }
    dispatch(taskActions.setSaveState({isLoading: true, isSuccess: false}));
    // dummy
    setTimeout(() => {
      dispatch(taskActions.setSaveState({isLoading: false, isSuccess: true}));
    }, 1000);
  };
};
```

あと hooks で state を引っ張ってくるときに必要になるので、modules 配下にある state の interface はまとめておく。

```ts:src/modules/index.ts
import { IState as TaskState } from "./taskModule";

export interface IRootState {
  task: TaskState;
}
```

## storeを作る
正確には、reducer をまとめた `rootReducer` と、store を生成するための `setupStore()` を作る。

```ts:src/store.ts
import { combineReducers, configureStore, getDefaultMiddleware } from "redux-starter-kit";
import taskModule from "./modules/taskModule";

const rootReducer = combineReducers({
  task: taskModule.reducer,
});

export const setupStore = () => {
  const middleware = getDefaultMiddleware();
  return configureStore({
    reducer: rootReducer,
    middleware,
  });
};
```

## componentを作る
props経由でリスト表示する component の例。
上記で実装した actions は `useDispatch()` 経由で呼び出す。

```tsx:src/components/todo_list.tsx
import React from "react";
import { useDispatch } from "react-redux";

import { ITask } from "../modules/taskModule";
import { taskActions } from "../modules/taskModule";

const TodoList: React.FC<{ list: ITask[], isEndTasks: boolean }> = ({ list, isEndTasks }) => {
  const dispatch = useDispatch();
  const endTask = (task: ITask) => dispatch(taskActions.endTask(task.id));
  const editTask = (task: ITask) => dispatch(taskActions.setEditTaskById(task.id));

  const targetList = list.filter((task) => {
    if (isEndTasks) {
      return task.isExecuted;
    } else {
      return !task.isExecuted;
    }
  });

  if (list.length < 1) {
    return null;
  }

  return (
    <table>
      <tbody>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Actions</th>
      </tr>
      {targetList.map((task) => {
        return (
          <tr key={task.id}>
            <td>{task.id}</td>
            <td>{task.name}</td>
            <td>
              <button onClick={() => editTask(task)} disabled={isEndTasks}>Edit</button>
              <button onClick={() => endTask(task)} disabled={isEndTasks}>End</button>
            </td>
          </tr>
        );
      })}
      </tbody>
    </table>
  );
};

export default TodoList;
```

## index.tsxで各componentとstoreを接続する
本当は form の部分とか諸々を components に移したほうが良いのだが、横着して index に置いている。
`setupStore()` を呼び出し `<Provider store={store}><App /></Provider>` とする部分は前とあまり変わってない。

state の取り出し方は hooks によって大きく変わった部分だが、書いてあるように `useSelecter()` で取り出したい state を選択するだけで取ってこれるのでそんなに難しくないと思う。

```tsx:src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { useDispatch, useSelector } from "react-redux";

import { setupStore } from "./store";
import { IRootState } from "./modules";
import TodoList from "./components/todo_list";
import { taskActions, saveTasks } from "./modules/taskModule";

const store = setupStore();

const App: React.FC = () => {
  const dispatch = useDispatch();
  const taskState = useSelector((state: IRootState) => state.task);

  const onChangeTaskName = (e) => dispatch(taskActions.setAddTaskName(e.target.value));
  const onChangeEditTaskName = (e) => dispatch(taskActions.setEditTaskName(e.target.value));
  const onClickAdd = (e) => {
    if (e) {
      e.preventDefault();
    }
    dispatch(taskActions.addTask());
  };
  const onClickEdit = (e) => {
    if (e) {
      e.preventDefault();
    }
    dispatch(taskActions.editTask());
  };
  const onClickSave = () => dispatch(saveTasks());

  return (
    <>
      <h1>Todo</h1>
      <h2>Add Task</h2>
      <form onSubmit={onClickAdd}>
        <input type="text" value={taskState.addTask.name} onChange={onChangeTaskName} />
        <button onClick={onClickAdd}>Add</button>
      </form>
      <h2>Current List</h2>
      <TodoList list={taskState.tasks} isEndTasks={false} />
      {taskState.editTask.id > -1 ? (
        <>
          <h2>Edit Task</h2>
          <form onSubmit={onClickEdit}>
            ({taskState.editTask.id}):
            <input type="text" value={taskState.editTask.name} onChange={onChangeEditTaskName} />
            <button onClick={onClickEdit}>Edit</button>
          </form>
        </>
      ) : null}
      <h2>Executed List</h2>
      <TodoList list={taskState.tasks} isEndTasks={true} />
      <h2>Save</h2>
      <button onClick={onClickSave} disabled={taskState.saveState.isLoading}>
        {taskState.saveState.isLoading ? "Saving..." : "Save"}
      </button>
      <div style={{display: taskState.saveState.isSuccess ? "block" : "none", color: "#FF0000"}}>
        Save Complete!
      </div>
    </>
  );
};

ReactDOM.render(
  <Provider store={store}><App /></Provider>,
  document.getElementById("root"),
);
```

## 最終的なディレクトリ構造
```
src
├── index.tsx
├── store.ts
├── components/
│   └── todo_list.tsx
├── modules/
│   ├── index.ts
│   └── taskModule.ts
└── templates/
    └── index.html
```