ReactをTypeScriptで書ける環境で、ReduxのTutorialをしてみる

## はじめに
Reactを使うにあたって、Reduxにはさっさと入門しておいたほうがいいと思います。
フロントを始めた時に、こんなドキュメントがあればハマらなかっただろうな…と思うことを備忘します。
ただ、こんな記事を書いておいてなんですが、よくわからなければ**公式ドキュメントとエラーメッセージをよく読むのが解決への近道**です。

## 前提条件
https://qiita.com/IgnorantCoder/items/d26083d9f886ca66d4ae
が終わっている。
つまり、TypeScriptがビルドできるようになっている。

## 目指す姿
* Reduxの公式のTodo ListアプリをTypeScriptで書く

## 必要なモジュールをインストールする
それでは必要なモジュールをインストールします。

### Redux
fluxの実装のひとつで、Stateを一括で管理してくれるやつ。
こいつのおかげで、あそこのcomponentの値変えてどうのこうとのとかややこしいことする必要がない。
Reduxは型情報を持ってるので@typesは不要です。

```shell
npm install --save redux
```

### React Redux
ReactのcomponentとReduxのcontainerを繋いでくれる凄いやつ。

```shell
npm install --save react-redux
npm install --save-dev @types/react-redux
```

## TodoList書いていく
[参考](https://redux.js.org/basics/example-todo-list)

ここからはExampleをなぞってゆく。
ただ、ファイルの構成が例になぞっていない部分もあるので注意。

### Entory Point
ここで一括管理するようの領域を作っておきます。

```tsx:src/index.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createStore } from 'redux';

import App from './components/App';
import { rootReducer } from './modules';
import { Provider } from 'react-redux';

const store = createStore(rootReducer);

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

これだけだとビルドエラーが出てうるさいので、必要最低限な実装を置いていきます。

```tsx:src/components/App.tsx
import * as React from 'react';

const component: React.SFC = () => {
    return (
        <div/>
    );
};

export default component;
```

```ts:src/modules/index.ts
import { combineReducers } from 'redux';

export const rootReducer = combineReducers({});
```

### Action CreatorとReducer
ActionとReducerはまとめて作ってしまう方が作りやすいです。
ActionはStateに対する操作オブジェクト、ReducerはActionをStateに適用して新しいStateを作るものです。
自分の書き方を定めてしまえば、特に難しいことはありません。頭を使うのはpayloadに何をもたせよっかな〜くらいです。
それもReducerにこのActionぶちこんで新しいStateを作る時に必要な情報ってなんだっけ？くらいに思えば簡単に決められます。

元の例では最後のTodoのインデックスを状態として保持しているようですが、とりあえず配列の長さからとれるため、ここでは保持しません。
筆者はReducerの数だけディレクトリを掘るのが好きなので、todosとvisibilityFilterの2つのディレクトリとそれらをまとめるindex.tsxを作ります。

#### todos
ほとんど決まった形なのですが、どんな気持ちで書き下しているかコードコメントを付けておきます。

```ts:src/modules/todos/AddTodo.ts
import { Action } from 'redux';

export type AddTodoPayload = {    // todoを追加する時に必要なのはtodoの内容くらい
    text: string;
};

export interface AddTodoAction extends Action {
    type: 'ADD_TODO';
    payload: AddTodoPayload;
}

export const addTodo = (payload: AddTodoPayload): AddTodoAction => {
    return {
        payload,
        type: 'ADD_TODO',
    };
};
```

```ts:src/modules/todos/ToggleTodo.ts
import { Action } from 'redux';

export type ToggleTodoPayload = {    // todoをトグルする時に必要なのはどのtodoかの情報くらい
    id: number;
};

export interface ToggleTodoAction extends Action {
    type: 'TOGGLE_TODO';
    payload: ToggleTodoPayload;
}

export const toggleTodo = (payload: ToggleTodoPayload): ToggleTodoAction => {
    return {
        payload,
        type: 'TOGGLE_TODO',
    };
};
```

```ts:src/modules/todos/index.ts
import { addTodo, AddTodoAction } from './AddTodo';
import { toggleTodo, ToggleTodoAction } from './ToggleTodo';

type Actions
    = AddTodoAction
    | ToggleTodoAction;

export type State = {    // ページ全体で保持しとくべき情報はTodoの配列くらい
    todos: {
        id: number;      // 連番を振っておく
        text: string;
        completed: boolean;
    }[];
};

const init = (): State => {
    return {
        todos: [],
    };
};

export const reducer = (state: State = init(), action: Actions) => {
    switch (action.type) {
    case 'ADD_TODO':
        return {
            todos: [
                ...state.todos,
                {    // 既存の配列に新しいのを追加
                    id: state.todos.length,
                    text: action.payload.text,
                    completed: false,
                },
            ],
        };
    case 'TOGGLE_TODO':
        return {
            todos: state.todos.map((e) => {
                return e.id !== action.payload.id
                    ? e
                    : {    // 対象のidだけcompletedを反転
                        ...e,
                        completed: !e.completed,
                    };
            }),
        };
    default:
        return state;
    }
};

export const actionCreator = {
    addTodo,
    toggleTodo,
};
```

#### visibilityFilter
こっちは完了済みだけ表示とかするフィルター。フィルタは文字列じゃなくてストリングリテラル型にしてます。

```ts:src/modules/visibilityFilter/SetVisibilityFilter.ts
import { Action } from 'redux';

type ShowAll = {
    type: 'SHOW_ALL',
};

type ShowCompleted = {
    type: 'SHOW_COMPLETED',
};

type ShowActive = {
    type: 'SHOW_ACTIVE',
};

export type FilterType
    = ShowAll
    | ShowCompleted
    | ShowActive;

export const showAll = (): FilterType => {
    return {
        type: 'SHOW_ALL',
    };
};

export const showCompleted = (): FilterType => {
    return {
        type: 'SHOW_COMPLETED',
    };
};

export const showActive = (): FilterType => {
    return {
        type: 'SHOW_ACTIVE',
    };
};

export type SetVisibilityFilterPayload = {    // とりあえずフィルターセットしといて、プレゼンテーション層で見え方の調整する
    filter: FilterType;
};

export interface SetVisibilityFilterAction extends Action {
    type: 'SET_VISIBILITY_FILTER';
    payload: SetVisibilityFilterPayload;
}

export const setVisibilityFilter
    = (payload: SetVisibilityFilterPayload): SetVisibilityFilterAction => {
        return {
            payload,
            type: 'SET_VISIBILITY_FILTER',
        };
    };
```

```ts:src/modules/visibilityFilter/index.ts
import {
    FilterType, showAll,
    setVisibilityFilter, SetVisibilityFilterAction,
} from './SetVisibilityFilter';
export { FilterType, showAll, showCompleted, showActive } from './SetVisibilityFilter';

type Actions
    = SetVisibilityFilterAction;

export type State = {
    visibility: FilterType,
};

const init = (): State => {
    return {
        visibility: showAll(),
    };
};

export const reducer = (state: State = init(), action: Actions) => {
    switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
        return {
            visibility: action.payload.filter,
        };
    default:
        return state;
    }
};

export const actionCreator = {
    setVisibilityFilter,
};
```

#### RootReducerを追加
先程、とりあえず最低限だけ書いたsrc/modules/index.tsxがこれです。大幅に加筆します。
RootStateとrootReducerのキー名は一致していなくてはダメです！

```ts:src/modules/index.ts
import { combineReducers } from 'redux';
import * as Todos from './todos';
import * as VisibilityFilter from './visibilityFilter';

export type RootState = {
    todos: Todos.State;
    visibilityFilter: VisibilityFilter.State;
};

export const rootReducer = combineReducers({
    todos: Todos.reducer,
    visibilityFilter: VisibilityFilter.reducer,
});

export const actionCreator = {
    todos: Todos.actionCreator,
    visibilityFilter: VisibilityFilter.actionCreator,
};
```

### Presentation Component
普通のコンポーネントです。状態とか細かいことを気にせず、すべてPropsとして受け取って描画するように書けばいいです。なので、基本的にはStateless function componentとして書けるはずです。
元のドキュメントだとFooter内で未定義のコンポーネントを利用していて気持ち悪いので、そこだけ実装を後回しにします。
また、AddTodoをPresentation componentとContainer componentに分けますので、こちらにもAddTodoを実装します。

```tsx:src/components/Todo.tsx
import * as React from 'react';

type Props = {
    text: string;
    completed: boolean;
    onClick: () => void;
};

const component: React.SFC<Props> = (props: Props) => {
    return (
        <li
            onClick={props.onClick}
            style={{
                textDecoration: props.completed ? 'line-through' : 'none',
            }}
        >
            {props.text}
        </li>
    );
};

export default component;
```

```tsx:src/components/TodoList.tsx
import * as React from 'react';
import Todo from './Todo';

type Props = {
    todos: {
        id: number;
        text: string;
        completed: boolean;
    }[];
    toggleTodo: (id: number) => void;
};

const component: React.SFC<Props> = (props: Props) => {
    return (
        <ul>
            {props.todos.map(todo => {
                return (
                    <Todo
                        key={todo.id}
                        text={todo.text}
                        completed={todo.completed}
                        onClick={() => { props.toggleTodo(todo.id); }}
                    />
                );
            })}
        </ul>
    );
};

export default component;

```

```tsx:src/components/Link.tsx
import * as React from 'react';

type Props = {
    active: boolean;
    children: React.ReactNode;
    onClick: () => void;
};

const component: React.SFC<Props> = (props: Props) => {
    return (
        <button
            onClick={props.onClick}
            disabled={props.active}
            style={{
                marginLeft: '4px',
            }}
        >
            {props.children}
        </button>
    );
};

export default component;
```

```tsx:src/components/AddTodo.tsx
import * as React from 'react';

type Props = {
    onSubmit: (text: string) => void;
};

type State = {
    value: string;    // Inputの中身を保持するためにStateを持つことにする。もちろんReduxに逃がしても良い。
};

class Component extends React.Component<Props, State> {
    constructor(props: Props) {
        super(props);
        this.state = {
            value: '',
        };
    }

    handleChange(event: React.ChangeEvent<HTMLInputElement>) {
        event.preventDefault();
        this.setState({
            value: event.target.value,
        });
    }

    handleSubmit(event: React.FormEvent<HTMLFormElement>) {
        event.preventDefault();
        const text = this.state.value.trim();
        if (text === '') {
            return;
        }
        this.props.onSubmit(text);
        this.setState({ value: '' });
    }

    render() {
        return (
            <div>
                <form onSubmit={(e) => { this.handleSubmit(e); } }>
                    <input
                        onChange={(e) => { this.handleChange(e); }}
                        value={this.state.value}
                    />
                    <button type={'submit'}>
                        Add Todo
                    </button>
                </form>
            </div>
        );
    }
}

export default Component;
```

### Container Component
それではPresentation ComponentとReduxをつなぎます。
ここも書き方は決まっていて、

- ReduxのStateからコンポーネントのPropsを抽出する`mapStateToProps`を書く
- ReduxのDispatch ActionをコンポーネントのPropsに封じ込める`mapDispatchToProps`を書く
- `connect`する

だけです。
そのため、この`mapStateToProps`と`mapDispatchToProps`が返却するオブジェクトをマージした時に、対象のコンポーネントのPropsを充足している必要があります。
なぜかconnect関数がコンパイルエラーになる場合は、これが守られていないケースが多い気がします。困った場合は、

- パラメータの数
- パラメータ名
- パラメータの型
- 渡すコンポーネント

をチェックすると、だいたいどれか間違ってます。

注意すべきは、mapDispatchToPropsからはStateを触れないということくらいです。
mergePropsという親玉みたいなやつもいますが使いませんし、使わないと死ぬ状況にも陥ったことは今のところありません。

```tsx:src/containers/VisibleTodoList.tsx
import { connect } from 'react-redux';
import { Action, Dispatch } from 'redux';

import { actionCreator, RootState } from '../modules';
import TodoList from '../components/TodoList';

const mapStateToProps = (state: RootState) => {
    const filter = () => {
        switch (state.visibilityFilter.visibility.type) {
            case 'SHOW_ALL':
                return state.todos.todos;
            case 'SHOW_COMPLETED':
                return state.todos.todos.filter(e => e.completed);
            case 'SHOW_ACTIVE':
                return state.todos.todos.filter(e => !e.completed);
            default:
                throw new Error('Unknown filter.');
        }
    };
    return {
        todos: filter()
    }
};

const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
    return {
        toggleTodo: (id: number) => {
            dispatch(actionCreator.todos.toggleTodo({id: id}));
        }
    }
};

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList);
```

```tsx:src/containers/FilterLink.tsx
import { connect } from 'react-redux';
import { Action, Dispatch } from 'redux';

import { actionCreator, RootState } from '../modules';
import Link from '../components/Link';
import { FilterType } from '../modules/visibilityFilter';

type OwnProps = {
    filter: FilterType;
}

const mapStateToProps = (state: RootState, ownProps: OwnProps) => {
    return {
        active: ownProps.filter === state.visibilityFilter.visibility,
    };
};

const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: OwnProps) => {
    return {
        onClick: () => { dispatch(actionCreator.visibilityFilter.setVisibilityFilter({ filter: ownProps.filter, })) },
    };
};

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(Link);
```

```tsx:src/containers/AddTodo.tsx
import { connect } from 'react-redux';
import { Action, Dispatch } from 'redux';

import { actionCreator } from '../modules';
import AddTodo from '../components/AddTodo';

const mapStateToProps = () => {
    return {};
};

const mapDispatchToProps = (dispatch: Dispatch<Action>) => {
    return {
        onSubmit: (text: string) => {
            dispatch(actionCreator.todos.addTodo({text}));
        }
    }
};

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(AddTodo);
```

### 仕上げ
さきほど保留した、Footerと最初に取り敢えず配置したAppのリバイスをすれば完成です。

```tsx:src/component/Footer.tsx
import * as React from 'react';
import FilterLink from '../containers/FilterLink';
import { showAll, showCompleted, showActive } from '../modules/visibilityFilter';

const component: React.SFC = () => {
    return (
        <div>
            <span>Show: </span>
            <FilterLink filter={showAll()}>
                All
            </FilterLink>
            <FilterLink filter={showActive()}>
                Active
            </FilterLink>
            <FilterLink filter={showCompleted()}>
                Completed
            </FilterLink>
        </div>
    );
};

export default component;
```

```tsx:src/component/App.tsx
import * as React from 'react';
import AddTodo from '../containers/AddTodo';
import VisibleTodoList from '../containers/VisibleTodoList';
import Footer from './Footer';

const component: React.SFC = () => {
    return (
        <div>
            <AddTodo />
            <VisibleTodoList />
            <Footer />
        </div>
    );
};

export default component;
```

## まとめ
実装してみるとわかると思いますがReduxを使うと、module下にあるStateやAction CreatorとComponent下にあるViewに相当する部分が疎結合になります。
今回はReduxのTutorialに沿っての実装だったため、module側から実装しましたが、Component側から実装してもいいですし、複数人で開発するならば同時進行で開発しても良いと思います。

できあがったソース群はこちら
https://github.com/IgnorantCoder/webpack-typescript-react-redux-sample

## おまけ (ありがちなエラーの解決法)
### TypeScript
#### error TS2307: Cannot find module 'redux'.
tsconfig.jsonに`"moduleResolution": "node"`を追加

### TSLint
#### file should end with a newline
ファイルの最後に空行が必要

#### Missing trailing comma
jsonを作る時に、最後の要素のあとにもカンマが必要

```NG.ts
{
    hoge: "fuga",
    foo: "poo"
}
```

```OK.ts
{
    hoge: "fuga",
    foo: "poo",
}
```

#### Expected property shorthand in object literal
オブジェクトリテラルの省略記法を使う

```NG.ts
const hoge: string = 'fuga';
const foo: string = 'poo';
{
    hoge: hoge,
    foo: foo,
}
```

```OK.ts
const hoge: string = 'fuga';
const foo: string = 'poo';
{
    hoge,
    foo,
}
```

### Runtime Error
#### Uncaught TypeError: Cannot read property 'setState' of undefined
classのメンバに暗黙にthisがバインドされないためです。呼び出す時にアロー表記にすれば解決。

#### Uncaught TypeError: xxx.map is not a function
なんでや！配列をmapさせろや！！！十中八九xxxにObjectが入ってしまっています。