LoginSignup
6
3

More than 3 years have passed since last update.

ReduxをTypeScriptで入門する on June, 2019

Posted at

こちらの続き

Redux

Reactの状態管理は、Reduxがベストプラクティスとのこと。
状態管理をReduxでやる方法を入門します。

なんやのRedux

  • Reactの状態であるStateを管理するもの
    • Storeというもので状態を一元管理する

たしかに、React書きはじめると、stateをどんどん上に持たせたくなるので、その延長と捉えると良さそうです。

How to use with TypeScript?

typescript-fsaというパッケージを使えば、Actionの静的型付けをよしなにしてくれるとのこと。

環境づくり

$npm install -S redux react-redux typescript-fsa typescript-fsa-reducers

Action、 Action Creator

  • 何らかの変更を通知するのがAction
  • Actionを作ってくれる関数がAction Creator
Action
{
  type: 'ADD_TODO', // ここがType
  text: 'Build my first Redux app' // ここがPayload
}

上記のようなObject=Actionをやり取りするわけですね。
これを関数でラップしたようなのがActionCreator。

ActionCreator
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

typescript-fsaでは、これをひとまとめにきれいにしてくれて、

typescript-fsaを利用した場合
import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();

export const todoActions = {
  addTodo: actionCreator<string>('ACTIONS_ADD_TODO')
};

と書けるようです。
ここのactionCreator<T> にPayloadの型を指定しておくと、ジェネリクスにより型情報が伝わる仕組みです。

Reducer

  • Actionを受け取ったら、加工して、新しい状態のStateを返すものがReducer
    • 状態用のロジック
    • これによってStateを更新する

今回はtypescript-fsa-reducersを使います。

ActionCreator & Reducerを一緒に

src/module/Todo.tsx
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory();

export const addTodo = actionCreator<string>('todo/ADD_TODO');
export const toggleTodo = actionCreator<number>('todo/TOGGLE_TODO');
export const ActionCreator = {
  addTodo,
  toggleTodo,
};

export interface State {
  id: number,
  text: string,
  completed: boolean,
}

const initialTodoState: State[] = [];

export const Reducer = reducerWithInitialState(initialTodoState)
  .case(addTodo, (state, payload) => {
    return [
      ...state,
      {
        id: state.length,
        text: payload,
        completed: false
      }
    ]
  })
  .case(toggleTodo, (state, payload) => {
    return state.map(todo => {
      return todo.id === payload ? { ...todo, completed: !todo.completed } : todo;
    })
  })
  .default((state) => {
    return state;
  });

ActionCreatorとReducerのファイルを分けておくほうが良いようですが、今回は1つにまとめています。

Store

  • データの保存場所がStore
    • Storeの生成はcreateStore()でReducerから生成する

いったんRootState, rootReducerとして、StateやReducerをまとめておきます。
ReducerをまとめるときはcombineReducers()を使います。

src/module/index.tsx
import { combineReducers } from 'redux';
import * as Todo from './Todo';
import * as VisibilityFilter from './VisibilityFilter';

export type RootState = {
  todos: Todo.State[],
  visibilityFilter: VisibilityFilter.State
}

export const rootReducer = combineReducers({
  todos: Todo.Reducer,
  visibilityFilter: VisibilityFilter.Reducer
})

export const store = createStore(rootReducer);

export const actionCreator = {
  todos: Todo.ActionCreator,
  visibilityFilter: VisibilityFilter.ActionCreator
};

Presentational Components

  • Reduxに依存しないコンポーネント
  • Propsを受け取ってレンダリングされるView
src/components/TodoList.tsx
import * as React from 'react';
import { State } from '../module/Todo';
import Todo from './Todo'


type OwnProps = {
  toggleTodo: (id: number) => void;
}

export type Props = { Todos: State[] } & OwnProps;

const TodoList: React.FunctionComponent<Props> = (props) => (
  <ul>
    {props.Todos.map(todo => (
      <Todo
        key={todo.id}
        id={todo.id}
        text={todo.text}
        completed={todo.completed}
        onClick={() => {
          props.toggleTodo(todo.id)
        }} />
    ))}
  </ul>
);

export default TodoList;

Stateは持っていないので、Propsを受け取ってViewを作るだけの処理にするようです。
Propsの型は自前で定義しますが、すでにReducerやActionCreatorで定義されているものがあれば、読み込んでおけば楽になりそう。

Container

  • 一番上のレイヤーのコンポーネント
  • ReduxとつながっているView
  • 最小のアプリケーション単位としてContainerが1つあるイメージっぽい
import { Dispatch } from 'redux';
import { connect } from 'react-redux'

import { RootState } from '../module/index';
import { ActionCreator } from '../module/Todo';
import TodoList from '../components/TodoList';

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

const mapDispatchToProps = (dispatch: Dispatch) => {
    return {
        toggleTodo: (id: number) => {
            dispatch(ActionCreator.toggleTodo(id));
        }
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList)

mapStateToProps

Storeから必要なものを抽出して、コンポーネントにわたす処理を行う。

const mapStateToProps = (store: Store) => ({ user: store.user });
connect(mapStateToProps)(Component);
  • これでStoreから取り出された{ user: store.user }Componentに渡る
  • Component側ではPropsに渡っていく

mapDispatchToProps

ActionをComponentに紐付ける処理を行う。

const mapDispatchToProps = (dispatch: Dispatch) => {
  return {
    updateName: (name: string) => dispatch(ActionCreator.updateName(name))
  }
}
connect(null, mapDispatchToProps)(Component);
  • 事前に定義していたActionCreatorをdispatchする
  • Component側ではPropsに関数として渡っていく

mergeProps

  • mapStateToProps, mapDispatchToPropsの上位概念
  • 渡すPropsObject.assignで作る
    • マニュアルで生成するようなもの

参考文献

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