Redux ExampleのTodo Listをはじめからていねいにを@reduxjs/toolkitで

## 概要

[前回][*3]、Immerを使った書き方を試してみたが、これは [@reduxjs/toolkitに組み込まれているらしい。*][*1]
なので、redux.js/toolkitを使った書き方を試してみる。
対象は未来の自分、あるいはReact触り始めたくらいの人。
「[Redux ExampleのTodo Listをはじめからていねいに(1)][*7]」の流れがほんとうに丁寧なのでなぞっていく。
docker上で動かしている点については[前回][*3]と同様のため割愛。


## 環境

- Windows 10 Home
- virtualbox 6.1.2.20200116
- Vagrant 2.2.7
- ubuntu 18.04 LTS Linux ubuntu-bionic 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
- docker-ce Docker version 19.03.5, build 633a0ea838
- docker-compose version 1.25.3, build d4d1b42b
- node 13.7
  - "@reduxjs/toolkit": "^1.2.3"
  - "react": "^16.12.0"
  - "react-dom": "^16.12.0"
  - "react-scripts": "^3.3.1"
  - "typescript": "^3.7.5"
  - "@types/react": "^16.9.19"
  - "@types/react-dom": "^16.9.5"
  - "@types/react-redux": "^7.1.7"
  
## 1. Hello World

* まずはHello Worldを表示するところから。
* `index.ts`が最上位のエンドポイント。Reactのコンポーネントをラップしている。

```ts:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

// reactのコンポーネントを#root以下に作成する
ReactDOM.render(<App />, document.getElementById("root"));
```

* 上記のファイルは以下のHTMLで読み込まれることを想定。（※DOM要素のIDがrootのものがあることに注目）

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="initial-scale=1" />
  <title>TODO</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
```

* ReactのコンポーネントはHello Worldを表示するだけ。

```ts:components/App.tsx
import React from "react";

// React.FC は React.FunctionComponent の短縮形
// @types/reactとlib.dom.d.tsで型が衝突することがあるため、慣習としてReactの型はnamed import({FC} from 'react'みたいなやつ)を避ける
const App: React.FC = () => {
  return <div> Hello World!!! </div>;
};
export default App;
```

[この時点のソース](https://github.com/hibohiboo/develop/tree/dfbca640b67c798e976fa4108320f96a5f21bee6/tutorial/lesson/react-my-examples/my-react-ts/)

## 2. actionCreatorで発行したactionをreducerに渡してstoreのstateを更新する

### Actions
* addTodoというactionCreatorを作る
* actionCreatorは関数 
  * actionを発行するだけ
* actionとはactionTypeとデータで構成されるオブジェクトのこと
  * actionType = ADD_TODO 
  * データ = {id: 数字, text:文字列}
  * [FSA](https://qiita.com/kabosusoba/items/e00af944529a5bf89d23)という推奨された規約がある
      * データをpayloadで渡す制約
      * payloadの型はなんでもよい。
          *  payload: string
          *  payload: number
          *  payload: {id: number, text: string}
* [参考:createAction](https://github.com/reduxjs/redux-toolkit/blob/master/docs/api/createAction.md) , [createAction(ts)](https://github.com/reduxjs/redux-toolkit/blob/master/docs/usage/usage-with-typescript.md#createaction)

* actionCreatorによって作られたactionはdispatchに渡される（後述）


```ts:actions/index.ts
import { createAction } from '@reduxjs/toolkit';

let nextTodoId = 0;
export const addTodo = createAction('ADD_TODO', (text: string) => ({ payload: { id: nextTodoId++, text } }));
```

### Reducers
* 純粋な関数
  * 現在のstateとactionを受け取り、新しいstateを返す。
  * 状態を持ってはいけない（＝スコープの外から変数を持ってくるのはNG。）
     *  例えば、actionで使っていたnextTodoIdのようなことはダメ。
* todoというreducerの動作について
  * actionTypeがADD_TODOのとき
  * { id: action.id, text: action.payload }という新しいstateを返す。
* [参考: createReducer](https://github.com/reduxjs/redux-toolkit/blob/master/docs/api/createReducer.md), [createReducer(ts)](https://github.com/reduxjs/redux-toolkit/blob/master/docs/usage/usage-with-typescript.md#createreducer)
* createReducerの第一引数には初期状態を入れておく
* createReducerでbuilderを使うと、typescriptを使っている場合、actionの型を自動で推測してくれる

```ts:reducers/index.ts
import { createReducer } from '@reduxjs/toolkit';
import { addTodo } from "../actions";

function initialState() {
  return { id: 0, text: '' };
}

export const reducer = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => (action.payload))
);
```

### Store
* アプリケーションで単一
* stateを保持する。
* configureStore関数でreducerを呼び出すことで作られる。
* [参考：configureStore](https://github.com/reduxjs/redux-toolkit/blob/master/docs/api/configureStore.md#usage)

```tsx:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';

const store = configureStore({ reducer });

ReactDOM.render(<App />, document.getElementById("root"));
```

#### 流れを確認
```diff:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';
+ import { addTodo } from "./actions";

const store = configureStore({ reducer });


+ store.dispatch(addTodo("Hello World!"));
+ console.log(store.getState());

ReactDOM.render(<App />, document.getElementById("root"));
```

* action -> reducer -> storeの一連の流れ
  * 実際にstateが更新されるのを見る

1. addTodo('Hello World!')
  * actionCreator
  * {payload: { id: 0, text: 'Hello World' }}というactionを作成
2.  store.dispatch(addTodo("Hello World!"));
  * dispatch関数にactionを渡す
  * todo reducerの引数にstateとactionが渡される
     * state = 現在のstate(初期値の{id:0, text ''})
     * action = {payload: { id: 0, text: 'Hello World' }}, 
  * todo reducerでは、新しいstate = { id: 0, text: 'Hello World' }を返す。
3. store.getState()
  * storeが保持しているstateを取得

##### chrome developer toolで確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/ea5a07b8-c652-23c9-83c2-72d5f60abc53.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/207add48d60763e5cda1209b56343f3930061d96/tutorial/lesson/react-my-examples/my-react-ts/)


## 3. storeで保持したstateをViewで表示する

### Todo Listの作成
#### reducerを書き換える
##### todoのreducerを別ファイルに外だし

* stateでtodoを保持することはできた
* このままでは1つのtodoしか保持できない
* 複数のtodoを保持できるよう拡張
* 今後のことを考えtodosのreducersを別ファイル（index.js -> todos.js）にする。
  * exampleではtodos以外のreducerを使うため
  * 1つしか使わない場合は別ファイル不要
* `state.push`で、一見stateを直接書き換えているように見えるが実は新しいstateを作って返している
  * 内部的に[immer](https://github.com/immerjs/immer)を使っているため
    * [参考:Direct State Mutation](https://github.com/reduxjs/redux-toolkit/blob/master/docs/api/createReducer.md#direct-state-mutation)
    * immerを使いたい場合、returnはしないようにすること
      * たとえば、`(state, action) => state.push(action.payload)`の書き方だと[暗黙的なreturn](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions#Function_body)になり、immerを使用しない
         * （[参考：voidを使えば、アロー関数 で｛｝が省ける。](https://asterisks.netlify.com/2016/04/01/js-void/)）
      * 「returnする場合、従来のreducerを使用」「returnしない場合、immerを使用」という動きっぽい。
      
```ts:@types/index.d.ts
export interface ITodo {
  id: number;
  text: string;
}
```

```ts:reducers/todos.ts
import { createReducer } from '@reduxjs/toolkit';
import { addTodo } from '../actions';
import { Todo } from '../@types';


function initialState(): ITodo [] {
  return [];
}

const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(action.payload);
    })
);
export default todos;
```

##### reducer/index.tsで外だししたreducerを統合

* `todo`の`reducer`を別ファイルにしたため、`reducers/index.ts`は各`reducer`を統合する役割にする
* 「`configureStore`の`reducer`プロパティ」に、「各reducerをプロパティにしたオブジェクト」を入れると、内部的にcombineReducersの挙動をする
  * `createStore(combineReducers(todos)) = configureStore(reducer:{todos})`
     * [（参考：configureStore reducer）](https://github.com/reduxjs/redux-toolkit/blob/master/docs/api/configureStore.md#reducer)
* combineReducers
  * 複数のreducersを結合する
  * reducerが結合された場合のstateの挙動も変化する
* storeで作成されるstateの違い
  * createStore(todos)
     * state = [todo1, todo2]
  * createStore(combineReducers(todos))
     * state = {todos = [todo1, todo2]}


```ts:reducers/index.ts
import todos from "./todos";

const rootReducer = { todos };
export default rootReducer;
```

[この時点のソース](https://github.com/hibohiboo/develop/tree/2ce440a63210ca26352722a62319e35605206f3a/tutorial/lesson/react-my-examples/my-react-ts/)

### ToDoコンポーネントとToDoListコンポーネントを作る
#### Todoコンポーネント
* propとして渡されてきたtextを表示する

```tsx:components/Todo.tsx
import React from "react";

const Todo: React.FC<{ text: string }> = ({ text }) => {
  return <li>{text} </li>;
};

export default Todo;
```
#### TodoListコンポーネント
* todosを取得する
  * [（参考：useSelector）](https://react-redux.js.org/next/api/hooks#useselector-examples)
  * [（例：Redux Starter KitでHooksとReduxを使いこなそう）](https://qiita.com/uehaj/items/f91d68ea4ef8450fc45c#%E3%82%B3%E3%83%BC%E3%83%89%E4%BE%8B)
* todosの各要素をTodoコンポーネントに渡す
  * 配列としてコンポーネントを複数生成するときkeyが必要になる
     * {...todo}はtodoのすべての要素
          * id={todo.id} text={todo.text}と同じ
          * [参考：スプレッド構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax) 

```tsx:components/TodoList.tsx
import React from "react";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  return (
    <ul>
      {todos.map(todo => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
};

export default TodoList;
```

* reducerと一緒にselectorも定義してしまう

```diff:reducers/todos.tsx
import { createReducer } from '@reduxjs/toolkit';
+ import { useSelector } from "react-redux";
import { addTodo } from '../actions';
import { ITodo } from '../@types';

function initialState(): ITodo[] {
  return [];
}

const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(action.payload);
    })
);
export default todos;


+ export const useTodoItems = () => {
+   return useSelector((state: { todos: ReturnType<typeof todos> }) => state.todos);
+ }
```

#### Providerの設定

* 作成したstoreをコンポーネントで使用するため、一番大きい単位を`<Provider>`で囲む
* `Provider`の属性に作成したstoreを指定する

```ts:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { configureStore } from '@reduxjs/toolkit';
import App from "./components/App";
import reducer from './reducers';
import { addTodo } from "./actions";


const store = configureStore({ reducer });
store.dispatch(addTodo("Hello World!"));
console.log(store.getState());

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

#### ブラウザで確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/732a6cfb-7065-c88f-7077-aa8ad29716e9.png)


[この時点のソース](https://github.com/hibohiboo/develop/tree/f43709ae1a3fc807fb5b703603e45b36b2237661/tutorial/lesson/react-my-examples/my-react-ts/)

## 4. フォームからtodoを追加
* フォームからtodoを追加する
* [（参考：Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。 ( Redux Toolkit)）](https://qiita.com/Ouvill/items/a76e9cbce569d01f2931#%E8%A3%9C%E8%B6%B3)
* buttonをクリック
  * dispatch(addTodo(input))でinputに入ってる値をtodoに追加
* inputはReact hookの仕組みを使用
  * [（参考：React hook）](https://ja.reactjs.org/docs/hooks-reference.html)
  * 初期値は""
  * `onChange`イベントで`setInput(e.target.value)`から`input`は更新される
 
```tsx:components/AddTodo.tsx 
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "../actions";

const AddTodo: React.FC = props => {
  // local state
  const [input, setInput] = useState("");
  // dispatch を用意
  const dispatch = useDispatch();
  // ハンドラーを用意。タスクを追加したらテキストエリアのクリア
  const clickHandler = () => {
    if (input !== "") {
      dispatch(addTodo(input));
      setInput("");
    }
  }

  return (
    <div>
      <input
        type="text"
        onChange={e => {
          setInput(e.target.value);
        }}
        value={input}
      />
      <button onClick={clickHandler}>add</button>
    </div>
  );
};

export default AddTodo;
```

* App.tsxに`AddTodo`コンポーネントを追加


```tsx:components/App.tsx
import React from "react";
import AddTodo from './AddTodo';
import ToddoList from './TodoList';

const App: React.FC = () => {
  return <div>
    <AddTodo />
    <ToddoList />
  </div>;
};

export default App;
```

##### ブラウザで確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/250939b0-954a-5a91-c07d-737b13d9827e.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/8953ab543e40f73708e2a744c8748589a80319cd/tutorial/lesson/react-my-examples/my-react-ts/)

## 5. 完了・未完了を表すcompletedによってスタイルを変える
ここからは、Todoの完了・未完了を切り替える「Toggle Todo」の機能を作っていく。

### todoにcompleted要素を追加して、とりあえず取り消し線を表示する
* completed要素を追加
  * todoごとに完了・未完了を区別するため
  * デフォルトはfalse
    * todo作成時は、未完了のため

```diff:@types/index.d.ts
export interface ITodo {
  id: number;
  text: string;
+  completed: boolean;
}
```


```diff:reducers/todos.ts
const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
-      state.push(action.payload);
+      state.push(
+        {
+          completed: false,
+          ...action.payload,
+        });
    })
);
```

#### Todoコンポーネントを修正

* completedがtrueだったらtextDecorationをline-throughにする

```tsx:components/Todo.tsx
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, }> = ({ completed, text }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }}>
    {text}
  </li>;
};

export default Todo;
```


* 一時的にreducers/todos.jsのcompleted: falseをtrueにして動作確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/294f2566-16ee-eace-78c4-231eee8328bc.png)


[この時点のソース](https://github.com/hibohiboo/develop/tree/7e744e20e37be3d93e2c63f0de6ecfc5d8fd11e1/tutorial/lesson/react-my-examples/my-react-ts/)



### actionCreatorからcompleted要素を操作する
* action経由で取り消し線のON/OFFを行う

#### actionCreatorの追加

* actionCreatorで必要なのは、todoのidだけ

```diff:actions/index.ts 
import { createAction } from '@reduxjs/toolkit';

let nextTodoId = 0;
export const addTodo = createAction('ADD_TODO', (text: string) => ({ payload: { id: nextTodoId++, text } }));
+ export const toggleTodo = createAction('TOGGLE_TODO', (id: number) => ({ payload: id }));

```

#### reducerの追加

* payloadで送られたidと同じidの要素のcompletedを反転させる
* ここでは`.map`で要素を作り直しているため、immerは使用していない

```diff:reducers/todos.ts
const todos = createReducer(initialState(), builder =>
  builder
    .addCase(addTodo, (state, action) => {
      state.push(
        {
          completed: true,
          ...action.payload,
        });
    })
+    .addCase(toggleTodo, (state, { payload }) => state.map(todo => ({ ...todo, completed: todo.id === payload ? !todo.completed : todo.completed })))
);
```

* `dispatch`を呼んで、true/falseが変更されるのを確かめてみる

```diff:index.tsx
 import reducer from './reducers';
-import { addTodo } from "./actions";
+import { addTodo, toggleTodo } from "./actions";

 const store = configureStore({ reducer });
 store.dispatch(addTodo("Hello World!"));
 console.log(store.getState());
+store.dispatch(toggleTodo(0));

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/80ff1eab-be3d-b7cd-7ea6-4c4e7ba25692.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/c2db2dff0a35ccd6c293e7ebef0021467bbb3d0a/tutorial/lesson/react-my-examples/my-react-ts/)

## 6. クリックしてcompletedの値を変える
#### 子コンポーネントの修正
* onClickイベントで親から渡されたtoggle関数を実行する

```tsx:components/Todo.tsx 
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, toggle: () => void }> = ({ completed, text, toggle }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }} onClick={toggle}>
    {text}
  </li >;
};

export default Todo;
```

#### 親コンポーネントの修正

```tsx:components/TodoList.tsx
import React from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  const dispatch = useDispatch();

  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={() => dispatch(toggleTodo(todo.id))} />)}
    </ul>
  );
};

export default TodoList;
````

[この時点のソース](https://github.com/hibohiboo/develop/tree/37c15b4adc63ebe5f99b6275846a2edd0c813639/tutorial/lesson/react-my-examples/my-react-ts/)


## 7 Filter Todo

* 表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」機能を作る
* 以下の3つのフィルターによって表示を変更する
  * SHOW_ALL: 全部表示
  * SHOW_COMPLETED: 完了しているtodoのみ
  * SHOW_ACTIVE: 完了していないtodoのみ
* 開発順は以下とする
  1. actionCreatorとreducerでフィルターの値をstore(state)に格納
  2. フィルターの値によってviewを変更（手動でフィルターを操作して動作確認）
  3. リンクをクリックしてフィルターを操作してviewを変更

### 7-1 actionCreatorとreducerでフィルターの値をstore(state)に格納

#### moduleの作成
* reducerとactionsを同時につくる

```ts:@types/index.d.ts
export type FilterType = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';
```

```ts:modules/visibleFilterModule.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { useSelector } from "react-redux";
import { FilterType } from '../@types';

function initialState(): FilterType {
  return 'SHOW_ALL';
}

// actions と reducers の定義
const visibilityFilterModules = createSlice({
  name: "visibilityFilter",
  initialState: initialState(),
  reducers: {
    visibilityFilter: (state, action: PayloadAction<FilterType>) => action.payload,
  }
});
export default visibilityFilterModules;
```

```ts:reducers/index.ts
import todos from "./todos";
import visibleFilterModule from '../module/visibilityFilterModules';
const rootReducer = { todos, visibleFilter: visibleFilterModule.reducer };
export default rootReducer;
```

#### 手動で動作確認

```tsx:index.tsx
// 省略
import visibilityFilterModules from './module/visibilityFilterModules';

const store = configureStore({ reducer });

console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_ALL"}
store.dispatch(visibilityFilterModules.actions.visibilityFilter('SHOW_COMPLETED'))
console.log(store.getState()) // => Object {todos: Array[0], visibilityFilter: "SHOW_COMPLETED"}

// 省略
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/be29b1da-4472-7ba2-4b4e-8dd4d4c5ec02.png)

[この時点のソース](https://github.com/hibohiboo/develop/tree/14140f062f396c7386e7b3a3e4442611429f3fb8/tutorial/lesson/react-my-examples/my-react-ts/)

### 7-2. フィルターの値によってviewを変更

```module/visibilityFilterModule.ts
// 省略
export const useVisibleFilter = () => {
  return useSelector((state: { visibleFilter: ReturnType<typeof visibilityFilterModules.reducer> }) => state.visibleFilter);
}
```

```tsx:components/TodoList.tsx
import React from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';
import { useVisibleFilter } from '../module/visibilityFilterModules';

const TodoList: React.FC = props => {
  let todos = useTodoItems();
  const dispatch = useDispatch();
  const filter = useVisibleFilter();

  switch (filter) {
    case 'SHOW_ALL':
      break;
    case 'SHOW_COMPLETED':
      todos = todos.filter((t) => t.completed);
      break;
    case 'SHOW_ACTIVE':
      todos = todos.filter((t) => !t.completed);
  }

  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={() => dispatch(toggleTodo(todo.id))} />)}
    </ul>
  );
};

export default TodoList;
```



### 7-3. リンクをクリックしてフィルターを操作してviewを変更

* リンクを表示させる
* dispatch(setVisibilityFilter())を呼び出す
* クリックしたときにonClickを呼ぶ

```tsx:components/Link.tsx
import React from "react";

const Link: React.FC<{ children: any, onClick: () => void }> = ({ children, onClick }) => {
  // eslint-disable-next-line jsx-a11y/anchor-is-valid
  return <a href="#" onClick={(e) => { e.preventDefault(); onClick(); }}>{children}</a>;
};

export default Link;
```

```tsx:components/Footer.tsx
import React from "react";
import { useDispatch } from "react-redux";
import Link from './Link';
import visibilityFilterModules from '../module/visibilityFilterModules';

const Footer: React.FC = () => {
  const dispatch = useDispatch();
  const visibilityFilter = visibilityFilterModules.actions.visibilityFilter;

  return <p>
    Show:
  {" "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ALL'))}>
      All
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ACTIVE'))}>
      Active
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_COMPLETED'))}>
      Completed
  </Link>
  </p>;
};

export default Footer;
```

```diff:components/App.tsx
import React from "react";
import AddTodo from './AddTodo';
import ToddoList from './TodoList';
+ import Footer from './Footer';

const App: React.FC = () => {
  return <div>
    <AddTodo />
    <ToddoList />
+    <Footer />
  </div>;
};

export default App;
```

#### activeな状態のリンクを押せないようにする

```tsx:components/Link.tsx
import React from "react";

const Link: React.FC<{ active: boolean, children: any, onClick: () => void }> = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>;
  }
  // eslint-disable-next-line jsx-a11y/anchor-is-valid
  return <a href="#" onClick={(e) => { e.preventDefault(); onClick(); }}>{children}</a>;
};

export default Link;
```


```tsx:components/Footer.tsx
import React from "react";
import { useDispatch } from "react-redux";
import Link from './Link';
import visibilityFilterModules, { useVisibleFilter } from '../module/visibilityFilterModules';

const Footer: React.FC = () => {
  const dispatch = useDispatch();
  const visibilityFilter = visibilityFilterModules.actions.visibilityFilter;
  const filter = useVisibleFilter();

  return <p>
    Show:
  {" "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ALL'))} active={filter === 'SHOW_ALL'}>
      All
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_ACTIVE'))} active={filter === 'SHOW_ACTIVE'}>
      Active
  </Link>
    {", "}
    <Link onClick={() => dispatch(visibilityFilter('SHOW_COMPLETED'))} active={filter === 'SHOW_COMPLETED'}>
      Completed
  </Link>
  </p>;
};

export default Footer;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/5ed4861b-2710-46b4-ed37-7b846f81cbfb.png)


[この時点のソース](https://github.com/hibohiboo/develop/tree/b5bbd9428b3f4fa50258826dfadee5f70f12e770/tutorial/lesson/react-my-examples/my-react-ts/)

これでactiveな状態のリンクを押せないようになり、「Filter Todo」機能が完成。

## 備考

### メモ化によるパフォーマンス改善について
* これくらいの小さいものなら不要そう。
* 子コンポーネントに dispatch を渡す場合、useCallbackを利用して、メモ化する
  * [（参考）](https://qiita.com/Ouvill/items/569384e5c8c7ce78f98e)
* 親の再レンダリングによって子コンポーネントが不必要に再レンダリングされることを回避するため

#### 子コンポーネントの修正
* onClickイベントで親から渡されたtoggle関数を実行する
* `React.memo`でpropsが変わらなかったときには変更されないようにする。
  * [（参考：React.memo）](https://ja.reactjs.org/docs/react-api.html#reactmemo) 

```tsx:components/Todo.tsx 
import React from "react";

const Todo: React.FC<{ completed: boolean, text: string, id: number, toggle: (id: number) => void }> = React.memo(({ completed, text, id, toggle }) => {
  return <li style={{ textDecoration: completed ? 'line-through' : 'none' }} onClick={() => toggle(id)}>
    {text}
  </li >;
});

export default Todo;
```

#### 親コンポーネントの修正
* useCallbackでメモ化する
  * [（参考：usecallback）](https://ja.reactjs.org/docs/hooks-reference.html#usecallback)
* dispatchでActionを呼び出す関数を子コンポーネントに渡す
* ここで`useCallback`を行うことにより、関数は不変なものとして子コンポーネントに渡される
  * 使わなかった場合、アロー関数は毎回違うオブジェクトと認識されてしまう。
      * せっかく、`React.memo`でpropsが変わらなかったら再レンダリングしないとしている意味がなくなる
      * `React.memo`や`useCallback`はバグのもとになりやすいので、パフォーマンス上問題なければ使わないほうが無難かも

```tsx:components/TodoList.tsx 
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { toggleTodo } from "../actions";
import Todo from "./Todo";
import { useTodoItems } from '../reducers/todos';

const TodoList: React.FC = props => {
  const todos = useTodoItems();
  const dispatch = useDispatch();
  const clickHandler = useCallback((id: number) => dispatch(toggleTodo(id)), [dispatch]);
  return (
    <ul>
      {todos.map(todo => <Todo key={todo.id} {...todo} toggle={clickHandler} />)}
    </ul>
  );
};

export default TodoList;
```

#### 効率化？
* memoを使った場合と、使わない場合で、dispatchの動作は、一見、変わっていないように見えた
  * DOMでの確認なので、内部的には違うのかも
* liがすべて書き換わるようだったら、問題だけど、これならパフォーマンスが問題になるまで対応不要かも。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4cfeda1c-ae61-12cf-fc97-a1836b48c082.png)


[関数メモ化時点のソース](https://github.com/hibohiboo/develop/tree/2f35e06e96d49e3a06418e300fa35cdd75916561/tutorial/lesson/react-my-examples/my-react-ts/)


## 環境詳細

### バージョン

#### OS
- Windows 10 Home

#### アプリケーション
```console
PS C:\WINDOWS\system32> clist -l | Select-String "virtualbox"
virtualbox 6.1.2.20200116
PS C:\WINDOWS\system32> vagrant -v
Vagrant 2.2.7
```

#### 仮想環境
[vagrantfile](https://github.com/hibohiboo/develop/blob/master/Vagrantfile)

```console
vagrant@ubuntu-bionic[]:~$ uname -a
Linux ubuntu-bionic 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
vagrant@ubuntu-bionic[]:~$ docker -v
Docker version 19.03.5, build 633a0ea838
vagrant@ubuntu-bionic[]:~$ docker-compose --version
docker-compose version 1.25.3, build d4d1b42b
```

#### インストールしたパッケージ

```json:package.json
{
  "name": "my-react",
  "version": "0.1.0",
  "private": true,
  "license": "MIT",
  "dependencies": {
    "@reduxjs/toolkit": "^1.2.3",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5"
  },
  "devDependencies": {
    "@types/jest": "^25.1.1",
    "@types/node": "^13.5.3",
    "@types/react": "^16.9.19",
    "@types/react-dom": "^16.9.5",
    "@types/react-redux": "^7.1.7",
    "@types/redux": "^3.6.0",
    "react-scripts": "^3.3.1",
    "typescript": "^3.7.5"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}
```




## 参考

[Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。 ( Redux Toolkit)][*1]
[フック API リファレンス][*2]
[Redux ExampleのTodo ListをはじめからていねいにをもういちどTypescriptとImmerで][*3]
[redux-toolkit][*4]
[Redux Starter KitでHooksとReduxを使いこなそう][*5]
[ハンバーガー屋で隣の女子高生が語るReact 第3話 Hooks][*6]
[Redux ExampleのTodo Listをはじめからていねいに(1)][*7]
[React HooksのuseCallbackを正しく理解する][*8]
[最近Reactを始めた人向けのReact Hooks入門][*9]
[雰囲気で使わない React hooks の useCallback/useMemo][*10]
[本当は怖いReact.memo][*11]
[Reduxの個人的チュートリアル(redux-toolkitあり)](https://qiita.com/yutaro-t/items/49bc849db32d76f7ff5c)

[*1]:https://qiita.com/Ouvill/items/a76e9cbce569d01f2931
[*2]:https://ja.reactjs.org/docs/hooks-reference.html
[*3]:https://qiita.com/hibohiboo/items/f1c4e33de05fb4b13b26
[*4]:https://github.com/reduxjs/redux-toolkit
[*5]:https://qiita.com/uehaj/items/f91d68ea4ef8450fc45c
[*6]:https://note.com/erukiti/n/ne91d53d64e86
[*7]:https://qiita.com/xkumiyu/items/9dfe51d2bcb3bdb06da3
[*8]:https://qiita.com/uehaj/items/99f7cd014e2c0fa1fc4e
[*9]:https://sbfl.net/blog/2019/11/12/react-hooks-introduction/
[*10]:https://qiita.com/seya/items/8291f53576097fc1c52a
[*11]:https://qiita.com/suzuesa/items/1bb3b1493ff526814f03#%E6%84%8F%E5%A4%96%E3%81%A8%E7%9F%A5%E3%82%89%E3%81%AA%E3%81%84%E7%AC%AC%E4%BA%8C%E5%BC%95%E6%95%B0%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9