LoginSignup
32
25

More than 3 years have passed since last update.

Redux ExampleのTodo ListをはじめからていねいにをTypescriptで(1)

Last updated at Posted at 2016-12-10

概要

Redux ExampleのTodo Listをはじめからていねいに(1)をTypescriptを使って行ったメモ。
Typescript, React, Reduxの練習ですので、間違っているところなどあったら教えていただけると嬉しいです。

2019/11/30 追記
Immerを試すために改めて環境作りました。

2020/2/3 追記

環境作成

エディタ

Visual Studio Codeを使用。*16

動作環境

  • windows10
  • vagrant1.8
  • virtualbox5.0
  • ubuntu-16.04
  • docker1.12
  • docker-compose1.8

仮想環境のIPは192.168.50.10に指定。

ブラウザはchromeで確認。

ディレクトリ構成(Hello world時)

redux-todo
  - bin # docker-composeの操作をシェル化
    - start.sh # 開発サーバの起動
    - build.sh # dist内にjsファイルをビルド
  - public # 開発サーバのベースとなるフォルダ
    - index.html # 開発サーバのホーム。
  - src
    - app.tsx     # エントリーポイント
    - components # Reactコンポーネント
      - App.tsx
  - webpack # ビルドツール
    - Dockerfile              # コンテナの環境設定ファイル
    - package.json            # コンテナ内にコピーされるnpm設定ファイル
    - webpack.config.js # ビルドツールの設定
    - tsconfig.json           # ビルドツールで利用するTypescript設定
  + dist   # ビルドされたファイルの格納先
  - docker-compose.yml # コンテナ起動時設定ファイル

ビルドツール

Webpackを使用。

docker-compose.yml
# buildtool_react_tsというコンテナ名で作成
buildtool_react_ts:
  # webpackディレクトリ内のDockerfileビルド
  build: ./webpack
  # webpackを使用するディレクトリを共有する。
  volumes:
   # ビルドするソースファイル
   - ./src:/my_webpack/src
   # ビルドファイルの出力先
   - ./dist:/my_webpack/dist
   # 開発用サーバのホームページに使用するhtml用ディレクトリ
   - ./public:/my_webpack/public
   # コンテナ上のpackage.jsonを上書き
   - ./webpack/package.json:/my_webpack/package.json
   # webpackの設定ファイル
   - ./webpack/webpack.config.js:/my_webpack/webpack.config.js
   # typescriptの設定ファイル
   - ./webpack/tsconfig.json:/my_webpack/tsconfig.json
  # ホストのポート8080をコンテナのポート8080にポートフォワーディング
  ports:
    - "8080:8080" # ホスト:コンテナでポート指定
  # docker-compose run を行ったときにコンテナ上で下のコマンドを行う
  command: [npm, run, start]

webpack/Dockerfile
# docker-hubからnode入りコンテナを取得
# https://hub.docker.com/_/node/
FROM node:7.2.0

# コンテナ上の作業ディレクトリ作成
WORKDIR /my_webpack

# 後で確認出来るようにpackage.jsonを作成
RUN npm init -y

# ビルドツール
RUN npm i --save-dev webpack@2.1.0-beta.27

# 開発用サーバ
RUN npm i --save-dev webpack-dev-server@2.1.0-beta.12

# jsViewライブラリreact
RUN npm i --save react
RUN npm i --save react-dom

# jsフレームワークredux
RUN npm i --save-dev redux
RUN npm i --save react-redux

# typescript
RUN npm i --save-dev typescript@next

# webpack用typescript loader
RUN npm i --save-dev ts-loader

# typescriptの型定義ファイル
RUN npm i --save-dev @types/react
RUN npm i --save-dev @types/react-dom
RUN npm i --save-dev @types/redux
RUN npm i --save-dev @types/react-redux
webpack/package.json
{
  "name": "my_webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --display-error-details",
    "start": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^0.14.54",
    "@types/react-dom": "^0.14.19",
    "@types/react-redux": "^4.4.35",
    "@types/redux": "^3.6.0",
    "redux": "^3.6.0",
    "ts-loader": "^1.3.1",
    "typescript": "^2.2.0-dev.20161210",
    "webpack": "^2.1.0-beta.27",
    "webpack-dev-server": "^2.1.0-beta.12"
  },
  "dependencies": {
    "react": "^15.4.1",
    "react-dom": "^15.4.1",
    "react-redux": "^4.4.6"
  }
}
webpack/webpack.config.js
var webpack = require('webpack');

module.exports = {
  // src以下のソースをビルド対象とする
  context: __dirname + '/src',
  // エントリーポイントとしてapp.jsを起点にビルドする
  entry: {
    typescript: './app.tsx'
  },
  // distにビルドしたファイルをbundle.jsの名前で保存
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },
  // importするときに、以下の配列に登録した拡張子は省略できる
  resolve: {
    extensions: [".js", ".ts", ".tsx"]
  },
  module: {
    rules: [
      // .ts, .tsxに一致する拡張子のファイルはts-loaderを通してトランスパイル
      { test: /\.tsx?$/, exclude: /node_modules/, loader: "ts-loader" }
    ]
  },
  // hot loadを有効にするためのプラグイン
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  // source-mapを出力して、ブラウザの開発者ツールからデバッグできるようにする。
  devtool: '#cheap-module-eval-source-map',
  // 開発サーバの設定
  devServer: {
    // public/index.htmlをデフォルトのホームとする
    contentBase: './public',
    // インラインモード
    inline: true,
    // 8080番ポートで起動
    port: 8080,
    // dockerのコンテナ上でサーバを動かすときは以下の設定で全ての接続を受け入れる
    host:"0.0.0.0",
    // hot loadを有効にする
    hot: true
  },
  // vagrantの仕様でポーリングしないとファイルの変更を感知できない
  watchOptions: {
    aggregateTimeout: 300,
    // 5秒毎にポーリング
    poll: 5000
  }
};
webpack/tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false,
        "jsx": "react"
    }
}

開発サーバ用html

開発サーバにはwebpack-dev-serverを利用する。

public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>Document</title>

</head>
<body>
  <!-- reactのコンポーネントを#root以下に作成する設定にしている -->
  <div id="root"></div>
  <!-- ビルドされたbundle.jsを読み込む -->
  <script src="bundle.js"></script>
</body>
</html>

shell

docker-composeのコマンドを毎回タイプするのが面倒なのでシェルにしている。

開発サーバの起動。

bin/start.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)

# docker-composeの起動。 docker-compsoe.ymlに記載されたcmdが実行される。
cd $bin_dir/../ && docker-compose up

webpack-dev-serverではメモリ上にビルドするので、ビルドファイルは出力されない。

ビルドファイルを出力するスクリプトの起動。

bin/build.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)

# docker-composeを起動し、コンテナ内で npm run buildを実行
cd $bin_dir/../ && docker-compose run buildtool_ts_react npm run build

Hello world

Hellor world

src/app.tsx
import * as React from 'react';
import { render } from 'react-dom';
import App from './components/App';

render(
  <App />,
  // reactのコンポーネントを#root以下に作成する
  document.getElementById('root')
);
src/components/App.tsx
import * as React from 'react';

class App extends React.Component<any, any> {
    render() {
        return <div> Hello World!!! </div>;
    }
}

export default App;

実行

開発用サーバを起動。

./bin/start.sh

ブラウザでアクセスして確認。

2. actionCreatorで発行したactionをreducerに渡してstoreのstateを更新する

Acitions

src/actions/index.js
import { Action } from 'redux';

export interface AddTodoAction extends Action {
    type: 'ADD_TODO';
    id: number;
    text: string;
}

let nextTodoId:number = 0;

// actionを発行する関数
export function addTodo(text:string) : AddTodoAction {
  // actionはtypeを持つオブジェクト
  // この場合、アクションタイプはADD_TODO
  // データはidとtextとなる。
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  }
}

Reducers

src/reducers/index.js
import { AddTodoAction } from '../actions';

export class TodoState {
  constructor(
    public id: number,
    public text: string
  ){}
}

// 現在のstateとactionを受け取り、新しいstateを返す関数
const todo = (state: TodoState, action: AddTodoAction) => {
  switch (action.type) {
    // actionTypeがADD_TODOのとき、
    // 新しいTodoStateを返す
    case 'ADD_TODO':
      return new TodoState(action.id, action.text);
    // それ以外のときはstateを変化させない
    default:
      return state
  }
}
export default todo

Store

app.tsx
// 省略
import { addTodo } from './actions'
let store = createStore(todo)

store.dispatch(addTodo('Hello World!'))
console.log(store.getState()) // => TodoState {id: 0, text: "Hello World!"}
render(
  // 省略
);

ここの時点のソース

github

3. storeで保持したstateをViewで表示する

TodoListの作成

TodoStateを他のソースからも参照するので、statesディレクトリを作成してそこに切り分けた。

states/TodoState.tsx
export default class TodoState {
  constructor(
    public id: number,
    public text: string
  ){}
}
reducers/todos.tsx
import { AddTodoAction } from '../actions';
import TodoState from '../states/TodoState';

// 現在のstateとactionを受け取り、新しいstateを返す関数
const todo = (state:any, action: AddTodoAction) => {
  switch (action.type) {
    case 'ADD_TODO':
      return new TodoState(action.id, action.text);
    // それ以外のときはstateを変化させない
    default:
      return state
  }
};

const todos = (state: TodoState[] = [], action: AddTodoAction) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        todo(undefined, action)
      ]
    default:
      return state
  }
};

export default todos;

ContainerとComponent

Stateless Functionsで書くのがよいらしいが、Typescriptだとどうすべきか。*9

2つの書き方を試してみる。

Todo.tsxはStateless Function。

components/Todo.tsx
import * as React from 'react';
import {PropTypes} from 'react';

interface IProps {
    text: string;
}

// propsを展開して分割代入
const Todo = ({ text }:IProps) => (
  <li>
    {text}
  </li>
);

// Todo.propTypesとするとProperty 'propTypes' does not exist on typeのエラーがでる。
Todo.prototype.propTypes = {
  text: PropTypes.string.isRequired
}

export default Todo;

TodoListはReact.Componentをextend。

TodoList.tsx
import * as React from 'react';
import Todo from './Todo';
import TodoState from '../states/TodoState';

// PropsをReact.Props<設定予定のコンポーネント>で継承して作ると補完が効く
// パラメータが足りないとエラーを吐く
interface IProps extends React.Props<TodoList> {
    todos: TodoState[];
}

class TodoList extends React.Component<IProps, {}> {
  constructor(public props: IProps) {
    super(props);
  }
  render(){
    return (
      <ul>
        {this.props.todos.map((todo) =>
          <Todo
            key={todo.id}
            {...todo}
          />
        )}
      </ul>
    );
  }
 }

 export default TodoList;

コンポーネントをconnectするコンテナ

containers/VisibleTodoList.tsx
import { connect } from 'react-redux';
import TodoList from '../components/TodoList';
import TodoState from '../states/TodoState';

interface IStateToProps {
    todos: TodoState[];
}

const mapStateToProps = (store:any): IStateToProps=> {
  return { todos: store.todos };
};

const VisibleTodoList = connect(
  mapStateToProps
)(TodoList);

export default VisibleTodoList;

ようやくブラウザに表示

components/App.tsx
import * as React from 'react';
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <VisibleTodoList />
  </div>
);

export default App;
app.tsx
import * as React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import App from './components/App';
import { createStore } from 'redux';
import todo from './reducers';
import { addTodo } from './actions'

let store = createStore(todo);
store.dispatch(addTodo('Hello React!'));
store.dispatch(addTodo('Hello Redux!'));

render(
  <Provider store={store}>
    <App />
  </Provider>,
  // reactのコンポーネントを#root以下に作成する
  document.getElementById('root')
);

この時点でのソース

github

4. フォームからtodoを追加

containers/AddTodo.tsx
import * as React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

interface IDispatch {
  // ?をつけないと以下のエラーが発生
  // Property 'dispatch' is missing in type 'IntrinsicAttributes & IDispatch'.
  dispatch?: any;
}

let AddTodo = ({ dispatch }:IDispatch) => {
  let input:HTMLInputElement;

  return (
    <div>
      <input ref={(node) => {
        input = node
      }} />
      <button onClick={() => {
        dispatch(addTodo(input.value))
        input.value = ''
      }}>
        Add Todo
      </button>
    </div>
  );
};

AddTodo = connect()(AddTodo);

export default AddTodo;

コメントで上記のエラーの修正をいただいた。感謝。

components/App.tsx
import * as React from 'react';
import VisibleTodoList from '../containers/VisibleTodoList'
import AddTodo from '../containers/AddTodo';

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
  </div>
);

export default App;

この時点でのソース

github

続きはRedux ExampleのTodo ListをはじめからていねいにをTypescriptで(2)

参考

Redux ExampleのTodo Listをはじめからていねいに
ReduxのTodo Listをdockerを使ってビルドする準備
VSCodeでTypescriptの型定義ファイルを設定したときのメモ
TypeScriptを使ってreactのチュートリアルを進めると捗るかなと思った。
React + TypeScript + Webpackの最小構成
Redux Example の TODO List を TypeScript で作成
npmでTypeScriptの型定義を管理できるtypesパッケージについて
TypeScript2.0での型定義ファイルの管理
Redux typed actions でReducerを型安全に書く (TypeScriptのバージョン別)
Reactチュートリアル: Intro To React【日本語翻訳】
Stateless な React Component の記法をまとめてみた
もうはじめよう、ES6~ECMAScript6の基本構文まとめ(JavaScript)~
React JSX with TypeScript(1.6)
TypeScript 1.8 のString literal typesでReactのPropを静的検証
TypeScript, React and Redux
TypeScriptでReactを書く(3):propTypes

32
25
2

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
32
25