こちらの続き
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
{
type: 'ADD_TODO', // ここがType
text: 'Build my first Redux app' // ここがPayload
}
上記のようなObject=Actionをやり取りするわけですね。
これを関数でラップしたようなのがActionCreator。
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
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を一緒に
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から生成する
- Storeの生成は
いったんRootState, rootReducerとして、StateやReducerをまとめておきます。
ReducerをまとめるときはcombineReducers()
を使います。
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
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
の上位概念 - 渡す
Props
をObject.assign
で作る- マニュアルで生成するようなもの