React+Redux+TypeScriptでサンプルアプリを作成する。

はじめに

React + Redux + TypescriptでTodoアプリを作っていきます。
今回は、Todoの追加のみです。

ソースコード(GitHub)
github.com

参考にしたサイト
qiita.com
react-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で配列を順に描画しています。