React+Redux+TypeScriptでサンプルアプリを作成する。
はじめに
React + Redux + TypescriptでTodoアプリを作っていきます。今回は、Todoの追加のみです。
ソースコード(GitHub)
github.com参考にしたサイト
qiita.comreact-redux.js.org
qiita.com
環境
- バージョン
node.js | -v8.12.0 |
react | -v16.9.0 |
redux | -v4.0.4 |
react-redux | -v7.1.1 |
- フォルダ構成
. |--/public | |--index.html | |--/src | |--/components | | |--AddTodoForm.tsx | | |--App.tsx | | |--TodoList.tsx | | | |--/containers | | |--AddTodoFormContainer.tsx | | |--TodoListContainer.tsx | | | |--actions.ts | |--index.tsx | |--reducers.ts | |--store.ts | |--package.json |--tsconfig.json |--webpack.config.js
Action
// src/actions.ts // typescript-fsa import actionCreatorFactory from 'typescript-fsa'; const actionCreator = actionCreatorFactory(); export const todoArrayActions = { addTodoAction: actionCreator<string>('ADD_TODO'), }; export const addTodoFormActions = { inputTextAction: actionCreator<string>('INPUT_TEXT'), initializeAddTodoFormAction: actionCreator('INITIALIZE_ADD_TODO_FORM'), };
typesctipt-fsaを使って、actionを書いていきます。
todoの配列に関するtodoArrayActionsと、todoを追加するフォームに関するaddTodoFormActionsでアクションを分けました。
reducerでTodoArrayとAddTodoFormが分かれているので、こちらも分けました。
Reducer
// src/reducers.ts // typescript-fsa import { reducerWithInitialState } from 'typescript-fsa-reducers'; // redux import { todoArrayActions, addTodoFormActions } from './actions'; export type Todo = { id: number; text: string; done: boolean; }; export type TodoArray = { todos: Todo[]; }; export const initialStateTodoArray: TodoArray = { todos: [ { id: 1, done: false, text: 'initial todo', }, ], }; export type AddTodoForm = { inputText: string }; export const initialStateAddTodoForm: AddTodoForm = { inputText: '' }; let idCounter: number = 1; const addTodo = (text: string): Todo => ({ id: ++idCounter, done: false, text, }); export const todoArrayReducer = reducerWithInitialState(initialStateTodoArray) .case(todoArrayActions.addTodoAction, (state, payload) => ({ ...state, todos: state.todos.concat( addTodo(payload) ), })) export const addTodoFormReducer = reducerWithInitialState(initialStateAddTodoForm) .case(addTodoFormActions.inputTextAction, (state, payload) => ({ ...state, inputText: payload, })) .case(addTodoFormActions.initializeAddTodoFormAction, () => ({ inputText: '', }))
type
大きく分けて、TodoArrayとAddTodoFormです。
reducers
- todoArrayReducer
addTodoAction: 受け取ったTodoをconcatにて、配列の一番うしろに追加します。
- addTodoFormReducer
inputTextAction: 入力されている文字をいれています。
initializeAddTodoFormAction: Todoを追加したあとに、ボックスの中身を初期化しています。
Store
// src/store.ts // redux import { createStore, combineReducers, compose, applyMiddleware } from 'redux'; import thunk from "redux-thunk"; import { TodoArray, todoArrayReducer, AddTodoForm, addTodoFormReducer, } from './reducers'; export type AppState = { todoArray: TodoArray addTodoForm: AddTodoForm }; const storeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const store = createStore( combineReducers<AppState>({ todoArray: todoArrayReducer, addTodoForm: addTodoFormReducer }), storeEnhancers(applyMiddleware(thunk)) ); export default store;
説明できるほど、わかっておりません。
以下の記事のstore.tsを参考にしてください。
qiita.com
index.tsx
// src/index.tsx // react import React from 'react'; import ReactDOM from 'react-dom'; // redux import store from './store'; // react-redux import { Provider } from 'react-redux'; // components import App from './components/App'; ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('app') );
Appコンポーネントに、Providerコンポーネント経由でstoreの値を渡しています。
特段、説明はいらないと思います。
Presentational and Container Components
このアプリの設計は、reduxの以下の設計思想に沿っています。redux.js.org
AddTodoFormContainer.tsx
// src/containers/AddTodoFormContainer.tsx // redux import { Dispatch } from "redux"; import { todoArrayActions, addTodoFormActions } from '../actions' import { AppState } from '../store'; // react-redux import { connect } from "react-redux"; // components import AddTodoForm from '../components/AddTodoForm' export type AddTodoFormHandler = { handleAddTodo(value: string): void handleInputText(value: string): void handleInitializeAddTodoForm(): void }; const mapStateToProps = (appState: AppState) => { return { inputText: appState.addTodoForm.inputText, }; }; const mapDispatchToProps = (dispatch: Dispatch) => { return { handleAddTodo: (value: string) => { dispatch(todoArrayActions.addTodoAction(value)) }, handleInputText: (value: string) => { dispatch(addTodoFormActions.inputTextAction(value)) }, handleInitializeAddTodoForm: () => { dispatch(addTodoFormActions.initializeAddTodoFormAction()) }, } }; export default connect( mapStateToProps, mapDispatchToProps )(AddTodoForm);
AddTodoFormに必要なactionとstateを記載しています。
TodoListContainer.tsx
// src/containers/TodoListContainer.tsx // redux import { AppState } from '../store'; // react-redux import { connect } from 'react-redux'; // components import { TodoList } from '../components/TodoList'; const mapStateToProps = (appState: AppState) => { return { todos: appState.todoArray.todos, }; }; export default connect(mapStateToProps, null)(TodoList);
TodoListに必要なactionとstateを記載しています。
AddTodoForm.tsx
// src/components/AddTodoForm.tsx // react import React from 'react'; // redux import { AddTodoForm } from '../reducers'; // containers import { AddTodoFormHandler } from '../containers/AddTodoFormConrainer'; type Props = AddTodoForm & AddTodoFormHandler; const AddTodoForm: React.FC<Props> = (props: Props) => { return ( <div> <input type='text' value={props.inputText} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { props.handleInputText(e.currentTarget.value) }} /> <button onClick={() => { props.handleAddTodo(props.inputText); props.handleInitializeAddTodoForm() }} > Add Todo </button> </div> ); }; export default AddTodoForm;
AddFormのviewです。
inputのvalueはinputTextに設定しています。
onChangeにて、入力内容をdispatchしています。
TodoList.tsx
// src/components/TodoList.tsx // react import React from 'react'; // redux import { TodoArray } from '../reducers'; type Props = TodoArray; export const TodoList: React.FC<Props> = (props: Props) => { return ( <div> {props.todos.map((todo) => ( <li key={todo.id.toString()}> {todo.id} {todo.text} </li> ))} </div> ); };
TodoListのviewです。
mapで配列を順に描画しています。