はじめに
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も行けそうという気になりました。
ではどのように書くのか
いきなり書きますとこうなります
export type Action =
{
type: 'ADD_TODO',
text: string
} |
{
type: 'COMPLETE_TODO',
index: number
} |
{
type: 'SET_VISIBILITY_FILTER',
filter: FilterType
};
ES2015と違う書き方になるのでかなり不安になりますが実際にチュートリアルを進めると
結構便利なのがわかります。
ActionCreaterを作ってみます
export function addTodo(text: string): Action {
return {
type: 'ADD_TODO',
txt
};
}
上記はtypoしてます。'ADD_TODO'
も補完が効くだけでなく
type === 'ADD_TODO'
の場合はメンバにtext
だけがあるということもEditor(VsCode)が解釈してくれます。
最終的に以下のように書いておきました
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込みの定義は以下となります
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
ここもソースだけ
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 = '';
}
};
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>
);
}
}
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>
);
}
}
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のときとほぼ同じです。
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;
}
以下全体のソースです
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