サイトアイコン 上尾市のWEBプログラマーによるブログ

「いまから始めるWebフロントエンド開発」の感想・備忘録2

「いまから始めるWebフロントエンド開発」の感想・忘備録1の続き

TODOアプリの作成

Reduxを使ったエントリポイントを作成する

  1. npm install --save redux
  2. エントリポイント(index.js)を作成
import { createStore } from 'redux'

const store = createStore(() => 'Hello Redux');
document.getElementById('contents').innerHTML = store.getState().toString();
  1. npx browserify -t babelify index.js -o bundle3.js

npm-scriptsのnpm run buildを上記コマンドにしておくと便利。

  1. index.htmlで bundle3.jsを読み込む

<div id="contents"></div>Hello Reduxが差し込まれる

Viewを実装する

  1. View(index.html)をとりあえず静的に作成
<div id="addTodo">
  <input type="text" name="" id="">
  <button type="button">追加</button>
</div>
<ul id="todoList">
  <li>11111</li>
  <li style="text-decoration: line-through">222222</li>
</ul>
<p id="links">
  Show: <a href="#">All</a>, <a href="#">Active</a>, <a href="#">Completed</a>
</p>

ActionCreatorを実装する

  1. actions/index.jsにActionCreatorを定義

actionsディレクトリに格納するのが一般的。

let nextTodoId = 0;

export const addTodo = (text) => {
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  }
};

export const toggleTodo = (id) => {
  return {
    type: 'TOGGLE_TODO',
    id
  }
};

export const setVisibilityFilter = (filter) => {
  return {
    type: 'SET_VISIBILITY_FILTER',
    filter
  }
};
// addTodo, toggleTodo, setVisibilityFilterがActionCreator=Actionを返す関数

エントリポイントでActionCreatorを使う

  1. エントリポイント(index.js)を修正(ViewとActionCreatorのつなぎこみ)

Viewにイベントがあった場合に「ActionCreatorからActionを取得しStoreに渡す」処理を追加する。
ReduxでStoreにActionを渡すには、Storeのdispatchメソッドを呼ぶ。

import { createStore } from 'redux'
import { addTodo, toggleTodo, setVisibilityFilter } from './actions/index'

const store = createStore(() => 'Hello Redux');

// 追加ボタンがクリックされたらActionCreator(addTodo)からActionを取得しStoreに渡す
document.querySelector('#addTodo button').addEventListener('click', () => {
  store.dispatch(addTodo(document.querySelector('#addTodo input').value));
});

// リストがクリックされたらActionCreator(toggleTodo)からActionを取得しStoreに渡す
document.querySelectorAll('#todoList li').forEach((li, index) => {
  li.addEventListener('click', () => {
    store.dispatch(toggleTodo(index));
  });
});

// フィルタリング用リンクがクリックされたらActionCreator(setVisibilityFilter)からActionを取得しStoreに渡す
document.querySelectorAll('#links a').forEach((a) => {
  a.addEventListener('click', () => {
    store.dispatch(setVisibilityFilter(a.textContent));
  });
});

Reducerを実装する

  1. reducers/index.jsにReducerを定義

reducersディレクトリに格納するのが一般的。
ReducerにはStoreから現在のStateとActionが渡され、新たなStateが返される。

import { combineReducers } from 'redux'

// 1つのTODOを処理するためのReducer
const todo = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text,
        completed: false
      };
    case 'TOGGLE_TODO':
      if (state.id !== action.id) {
        return state;
      }
      return Object.assign({}, state, {
        completed: !state.completed
      });
    default:
      return state;
  }
};

// 複数のTODOを処理するためのReducer
const todos = (states=[], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...states,
        todo(undefined, action)
      ];
    case 'TOGGLE_TODO':
      return states.map(state => todo(state, action));
    default:
      return states;
  }
};

// 表示状態を処理するためのReducer
const visibilityFilter = (state='SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter;
    default:
      return state;
  }
};

const todoApp = combineReducers({
  todos,
  visibilityFilter
});
export default todoApp;

Reducerの動作確認をする

  1. Reducerの動作確認をするためのコード
import todoApp from './reducers/index'

// 初期化時のstateとactionは空オブジェクトでなければならない
const initialState = todoApp({}, {});
console.log(initialState);

const secondState = todoApp(initialState, {type: 'ADD_TODO', id: 1, text: 'First todo'});
console.log(secondState);

const thirdState = todoApp(secondState, {type: 'SET_VISIBILITY_FILTER', filter: 'Active'});
console.log(thirdState);

npx browserify -t babelify reducer_test.js -o reducer_test_bundle.js でバンドルしてブラウザで確認。

動作結果

・初期化後
{
  todos: [],
  visibilityFilter: "SHOW_ALL"
}

・ADD_TODO後
{
  todos: [
    { id: 1, text: "First todo", completed: false }
  ],
  visibilityFilter: "SHOW_ALL"
}

・SET_VISIBILITY_FILTER後
{
  todos: [
    { id: 1, text: "First todo", completed: false }
  ],
  visibilityFilter: "Active"
}

エントリポイントでReducerを使う

  1. エントリポイント(index.js)を修正

ReducerをStoreに設定する。(createStore関数の引数にReducerを渡す)

import { createStore } from 'redux'
import { addTodo, toggleTodo, setVisibilityFilter } from './actions/index'
import todoApp from './reducers/index'

const store = createStore(todoApp);

// 追加ボタンがクリックされたらActionCreator(addTodo)からActionを取得しStoreに渡す
document.querySelector('#addTodo button').addEventListener('click', () => {
  store.dispatch(addTodo(document.querySelector('#addTodo input').value));
});

// リストがクリックされたらActionCreator(toggleTodo)からActionを取得しStoreに渡す
document.querySelectorAll('#todoList li').forEach((li, index) => {
  li.addEventListener('click', () => {
    store.dispatch(toggleTodo(index));
  });
});

// フィルタリング用リンクがクリックされたらActionCreator(setVisibilityFilter)からActionを取得しStoreに渡す
document.querySelectorAll('#links a').forEach((a) => {
  a.addEventListener('click', () => {
    store.dispatch(setVisibilityFilter(a.textContent));
  });
});

ViewでReactを使う

  1. npm install --save react react-dom react-redux prop-types

※ 書籍内ではprop-typesがないが、react15.5から別パッケージとなった

  1. npm install --save-dev @babel/preset-react

※ 書籍内ではbabel-preset-reactとなっているが古い。

  1. .babelrcに@babel/preset-reactを追加
{
  "presets": [
    [
      "@babel/preset-env", {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    "@babel/preset-react"
  ]
}
  1. li表示用コンポーネント(components/Todo.js)を作成
import React from 'react';
import PropTypes from 'prop-types';

class Todo extends React.Component {
  render() {
    return (
      <li onClick={this.props.onClick} style={{ textDecoration: this.props.completed ? 'line-through' : 'none' }}>{this.props.text}</li>
    )
  }
}
Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired,
};
export default Todo;

※ 書籍ではimport React, {PropTypes} from 'react';となっているが古い。

  1. ul表示用コンポーネント(components/TodoList.js)を作成
import React from 'react';
import PropTypes from 'prop-types';
import Todo from './Todo'

class TodoList extends React.Component {
  render() {
    return (
      <ul>
        {this.props.todos.map(todo =>
          <Todo key={todo.id} {...todo} onClick={() => this.props.onTodoClick(todo.id)}/>
        )}
      </ul>
    )
  }
}
TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
  }).isRequired).isRequired,
  onTodoClick: PropTypes.func.isRequired,
};
export default TodoList;
  1. フィルタリンク用コンポーネント(components/Link.js)を作成
import React from 'react';
import PropTypes from 'prop-types';

class Link extends React.Component {
  render() {
    return (
      <a href="" onClick={e => {
        e.preventDefault();
        this.props.onClick();}
      }>
        {this.props.children}
      </a>
    )
  }

Link.propTypes = {
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired,
};
export default Link;

ReactとReduxのつなぎこみ

  1. containers/AddTodo.jsを作成(入力部分のつなぎこみ)

つなぎこみ部分はcontainersディレクトリに格納するのが一般的。
コードは長くなるが、行っていることは基本的には以下の2つのことだけ。

import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';

class AddTodo extends React.Component {
  render() {
    let input;
    return (
      <div>
        <form onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }

          // ActionCreatorからActionを取得してStoreに渡す
          this.props.dispatch(addTodo(input.value));
          input.value = '';
        }}>
          <input ref={node => {
            input = node;
          }} />
          <button type="submit">追加</button>
        </form>
      </div>
    )
  }
}
export default connect()(AddTodo);
  1. containers/VisibleTodoList.jsを作成(リスト部分のつなぎこみ)
import { connect } from 'react-redux';
import { toggleTodo } from '../actions';
import TodoList from '../components/TodoList';

// filterの値により絞り込みを行う
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case 'SHOW_ALL':
      return todos;
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed);
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed);
  }
};

// StateをViewのプロパティに落とし込む
const mapStateToProps = (state) => {
  return {
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
  };
};

// ViewからStateにイベントを伝える
const mapDispatchToProps = (dispatch) => {
  return {
    onTodoClick: (id) => {
      // ActionCreatorからActionを取得しStoreに渡す
      dispatch(toggleTodo(id));
    }
  }
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);


containers/AddTodo.jsにはViewが含まれていたが、containers/VisibleTodoList.jsではViewがcomponents/TodoList.jsやcomponents/Todo.jsで用意されているため、Reduxとのつなぎこみ部分だけを実装している。

Reduxへの受け渡し処理をcontainersに書くことで、Viewはイベント処理をどこにどのように渡せばよいか知る必要がなくなり、純粋なViewとしての挙動のみを気にすればよい構成となる。

  1. containers/FilterLink.jsを作成(フィルタリング部分のつなぎこみ)
import { connect } from 'react-redux';
import {setVisibilityFilter, toggleTodo} from '../actions';
import Link from '../components/Link';

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  };
};
const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter));
    }
  }
};
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link);
  1. components/Footer.jsを作成(フィルタリング用リンクの表示)
import React from 'react'
import FilterLink from '../containers/FilterLink'

class Footer extends React.Component {
  render() {
    return (
      <p>
        Show: <FilterLink filter="SHOW_ALL">All</FilterLink>, 
        <FilterLink filter="SHOW_ACTIVE">Active</FilterLink>, 
        <FilterLink filter="SHOW_COMPLETED">Completed</FilterLink>
      </p>
    )
  }
}
export default Footer;

FilterLinkでLinkとReduxのつなぎこみを行い、表示するためのFooterを作成している。

View周辺のエントリポイントとなるファイルを作成

  1. components/App.jsを作成
import React from 'react'
import Footer from './Footer';
import AddTodo from '../containers/AddTodo';
import VisibleTodoList from '../containers/VisibleTodoList';

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
      </div>
    );
  }
}
export default App;

作成した3つのコンポーネントが含まれている。

最後にエントリポイント(index.js)とindex.htmlを変更

  1. エントリポイント(index.js)を変更
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

const store = createStore(todoApp);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<div id="root">
</div>
<script src="./bundle3.js"></script>

Expressで実行

  1. npm install --save express
  2. app.jsを作成
const express = require('express');
const app = express();

app.use(express.static('./'));
app.listen(3000, () => {
  console.log('listening on post 3000');
});
  1. node app.jsで実行
  1. http://localhost:3000で確認
モバイルバージョンを終了