Reduxの学習をTypeScriptとの組み合わせで進める


# はじめに

TypeScriptでReactの学習を薦めたらいい感じでした。
じゃあReduxはどうなんだろう？と思いいきなりTypescriptで始めようとしたら
Reactほど上手く行かず放置していました。

ところがReactNativeの参考としてのF8Appを見たところ内部ではReduxを使っており
Flowで実装されていて凄くTypeScriptみたいな構文で書かれていました。
それを丸パクリしてチュートリアルを薦めたらすんなりいけたので纏めてみます

[Redux入門 1日目 Reduxとは(公式ドキュメント和訳)](http://qiita.com/kiita312/items/b001839150ab04a6a427)の記事を順繰りに読みながら薦めていきます。

# 環境構築


js+reactの開発環境が``create-react-app``で簡単に作れるようになったように
以下のコマンドでreact+Typescript開発環境が作れます

```bash
$ create-react-app --scripts-version=react-scripts-ts <project名>                                                                                                                                                                     
$ cd <project名>
$ yarn start
```


## 追加パッケージ

```bash
$ yarn add --save redux react-redux
$ yarn add --save-dev @types/redux @types/react-redux
```

# Action/ActionCreaterの定義

ぶっちゃけここがメインです
F8Appの内容をパクったらいい感じで使い物になりTypeScriptでReduxも行けそうという気になりました。

ではどのように書くのか
いきなり書きますとこうなります

```ts:src/actions/types.ts
export type Action =
    {
        type: 'ADD_TODO',
        text: string
    } |
    {
        type: 'COMPLETE_TODO',
        index: number
    } |
    {
        type: 'SET_VISIBILITY_FILTER',
        filter: FilterType
    };

```

ES2015と違う書き方になるのでかなり不安になりますが実際にチュートリアルを進めると
結構便利なのがわかります。

ActionCreaterを作ってみます

```ts:src/actions/types.ts
export function addTodo(text: string): Action {
    return {
        type: 'ADD_TODO',
        txt
    };
}

```

上記はtypoしてます。`'ADD_TODO'`も補完が効くだけでなく
``type === 'ADD_TODO'`` の場合はメンバに`text`だけがあるということもEditor(VsCode)が解釈してくれます。

![types_ts_-_redux_ts.png](https://qiita-image-store.s3.amazonaws.com/0/3844/cdc1c563-1f11-af9a-05fe-0d9d0c04ffdc.png "types_ts_-_redux_ts.png")


最終的に以下のように書いておきました

```ts:src/actions/types.ts

export type FilterType = 'SHOW_ALL' | 'SHOW_COMPLETED' | 'SHOW_ACTIVE';
export type TodoType = {
    text: string;
    completed: boolean
};


export type Action =
    {
        type: 'ADD_TODO',
        text: string
    } |
    {
        type: 'COMPLETE_TODO',
        index: number
    } |
    {
        type: 'SET_VISIBILITY_FILTER',
        filter: FilterType
    };



export function addTodo(text: string): Action {
    return {
        type: 'ADD_TODO',
        text
    };
}

export function completeTodo(index: number): Action {
    return {
        type: 'COMPLETE_TODO',
        index
    };
}

export function setVisibilityFilter(filter: FilterType): Action {
    return {
        type: 'SET_VISIBILITY_FILTER',
        filter
    };
}

```

# Reducersの定義

TodoアプリのStateは以下のようなイメージなので

```
{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
```

TypeScriptでStateのスキーマを作ります。TypeScriptでReactを書くときはPropsやStateをインターフェース定義するので同じ感じです

```
export interface TodoAppState {
    visibilityFilter: FilterType;
    todos: TodoType[];
}
```

最終的なReducer込みの定義は以下となります

```ts:src/reducer.ts
import { Action, FilterType, TodoType } from './actions/types';
export interface TodoAppState {
    visibilityFilter: FilterType;
    todos: TodoType[];
}

const initState: TodoAppState = {
    visibilityFilter: 'SHOW_ALL',
    todos: []
};

export function todoApp(state: TodoAppState = initState, action: Action) {
    switch (action.type) {
        case 'SET_VISIBILITY_FILTER':
            return Object.assign({}, state, {
                visibilityFilter: action.filter
            });
        case 'ADD_TODO':
            return Object.assign({}, state, {
                todos: <TodoType[]> [
                    ...state.todos,
                    {
                        text: action.text,
                        completed: false
                    }
                ]
            });
        case 'COMPLETE_TODO':
            return Object.assign({}, state, {
                todos: <TodoType[]> [
                    ...state.todos.slice(0, action.index),
                    {
                        text: state.todos[action.index].text,
                        completed: !state.todos[action.index].completed
                    },
                    ... state.todos.slice(action.index + 1)
                ]
            });

        default:
            return state;
    }
}
```

# Reducerの分割

コレは省略します

# Presentational Components

ここもソースだけ

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

interface AddTodoProps extends React.Props<AddTodo> {
    onAddClick: (text: string) => void;
}

interface AddTodoStatus {
    text: string;
}
export default class AddTodo extends React.Component<AddTodoProps, AddTodoStatus> {
    // ref保持しとくフィールド
    private x: HTMLInputElement;
    render() {
        return (
            <div>
                <input type="text" ref={e => (this.x = e)} onChange={e => {
                        this.setState({ text: e.target.value });
                    }
                }/>
                <button onClick={e => this.handleClick.call(this)}>
                    Add
                </button>
            </div>);
    }

    handleClick() {
        this.props.onAddClick(this.state.text);
        this.setState({ text: '' });
        this.x.value = '';
    }
};
```


```ts:/src/components/Footer.tsx
import * as React from 'react';
import { FilterType } from '../actions/types';

interface FooterProps extends React.Props<Footer> {
    filter: FilterType;
    onFilterChange: (filter: FilterType) => void;
}
export default class Footer extends React.Component<FooterProps, {}> {
    renderFilter(filter: FilterType, name: string) {
        if (filter === this.props.filter) {
            return name;
        }

        return (
            <a href="#" onClick={
                e => {
                    e.preventDefault();
                    this.props.onFilterChange(filter);
                }
            } >
                {name}
            </a>
        );
    }

    render() {
        return (
            <p>
                Show:{' '}
                {this.renderFilter('SHOW_ALL', 'All')}
                {', '}
                {this.renderFilter('SHOW_COMPLETED', 'Completed')}
                {', '}
                {this.renderFilter('SHOW_ACTIVE', 'Active')}
                .
        </p>
        );
    }
}

```

```ts:src/components/Todo.tsx
import { Component, Props } from 'react';
import * as React from 'react';

interface TodoProps extends Props<Todo> {
    onClick: () => void;
    completed: boolean;
    text: string;
}
export default class Todo extends Component<TodoProps, void> {
    render() {
        return (
            <li onClick={this.props.onClick}
                style={{
                    textDecoration: this.props.completed ? 'line-through' : 'none',
                    cursor: this.props.completed ? 'default' : 'pointer'
                }}>
                {this.props.text}
            </li>
        );
    }
}
```

```ts:src/components/TodoList.tsx
import * as React from 'react';
import { Component, Props } from 'react';
import Todo from './Todo';

interface TodoListProps extends Props<TodoList> {
    onTodoClick: (index: number) => void;
    todos: { text: string, completed: boolean }[];
}
export default class TodoList extends Component<TodoListProps, void> {
    render() {
        return (
            <ul>
                {
                    this.props.todos.map((todo, index) => (
                        <Todo {...todo}
                            key={index}
                            onClick={() => this.props.onTodoClick(index)} 
                        />)
                    )
                }
            </ul>
        );
    }
}
```


# Connecting to Redux

index.tsはES2015のときとほぼ同じです。

```ts:index.ts
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './containers/App';
import {todoApp} from './reducer';
import { Provider } from 'react-redux';
import { createStore } from 'redux';


let store  = createStore(todoApp);
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root') as HTMLElement
);

```

Appだけ引っかかりましたconnectでラップしてdespatchをpropsに生やすことになるんですが
その場合のインターフェース定義の仕方がよくわかりませんでした
無理やり追加することにしました

```diff
interface AppProps extends React.Props<{}> {
++  dispatch: (action: Action) => void; //手動で無理やり追加
    visibleTodos: TodoType[];
    visivilityFilter: FilterType;
}
```

以下全体のソースです


```ts:src/containers/App.tsx
import * as React from 'react';
import * as ReactRedux from 'react-redux';
import AddTodo from '../components/AddTodo';
import TodoList from '../components/TodoList';
import Footer from '../components/Footer';
import { TodoAppState } from '../reducer';
import { addTodo, completeTodo, setVisibilityFilter, FilterType, TodoType, Action } from '../actions/types';

interface AppProps extends React.Props<{}> {
    dispatch: (action: Action) => void;
    visibleTodos: TodoType[];
    visivilityFilter: FilterType;
}

function selectTodos(todos: TodoType[], filter: FilterType): TodoType[] {
    switch (filter) {
        case 'SHOW_ACTIVE':
            return todos.filter(todo => (todo.completed === false));
        case 'SHOW_COMPLETED':
            return todos.filter(todo => (todo.completed === true));
        case 'SHOW_ALL':
        default:
            return todos;
    }
}
function select(state: TodoAppState) {
    return {
        visibleTodos: selectTodos(state.todos, state.visibilityFilter),
        visivilityFilter: state.visibilityFilter
    };
}


class App extends React.Component<AppProps, {}> {
    render() {
        const { dispatch, visibleTodos, visivilityFilter } = this.props;
        return (
            <div>
                <AddTodo
                    onAddClick={text => { dispatch(addTodo(text)); }} />
                <TodoList
                    todos={visibleTodos}
                    onTodoClick={index =>
                        dispatch(completeTodo(index))
                    } />
                <Footer
                    filter={visivilityFilter}
                    onFilterChange={filter =>
                        dispatch(setVisibilityFilter(filter))
                    } />
            </div>
        );
    };
};

export default ReactRedux.connect(select)(App);
```

#オマケ

現状では追加したToDoをクリックするとComplete状態に遷移するだけで戻せないので
トグルするようにします

```diff
--        case 'COMPLETE_TODO':
++        case 'TOGGLE_COMPLETE_TODO':
            return Object.assign({}, state, {
                todos: <TodoType[]> [
                    ...state.todos.slice(0, action.index),
                    {
                        text: state.todos[action.index].text,
--                        completed: true
++                        completed: !state.todos[action.index].completed
                    },
                    ... state.todos.slice(action.index + 1)
                ]
            });

```

VSCで簡単にリファクタリングできました。文字列もリファクタリング対象なのでいちいち文字列の定数定義は不要ってことなんでしょうか。

以下にコミットしてあります。
https://github.com/m0a-mystudy/redux_ts.git


参考:http://qiita.com/kiita312/items/b001839150ab04a6a427