LoginSignup
4
13

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-04-29

はじめに

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

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

Redux入門 1日目 Reduxとは(公式ドキュメント和訳)の記事を順繰りに読みながら薦めていきます。

環境構築

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

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

追加パッケージ

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

Action/ActionCreaterの定義

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

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

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を作ってみます

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

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

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込みの定義は以下となります

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

ここもソースだけ

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 = '';
    }
};
/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>
        );
    }
}

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>
        );
    }
}
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のときとほぼ同じです。

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に生やすことになるんですが
その場合のインターフェース定義の仕方がよくわかりませんでした
無理やり追加することにしました

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

以下全体のソースです

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状態に遷移するだけで戻せないので
トグルするようにします

--        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

4
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
13