前回に引き続き、CodeSandboxでReactのコードを書いていきます。
今回はTODOリストを作成します。
ところでJSXはずっと食わず嫌いだったんですが、正直五秒で慣れたので反省しています。
リストに新しい行を追加する処理
折角なのでreduxを使います。
https://redux.js.org/
index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import './style.css'
import App from './App'
import { store } from './reducers/reducers'
render(
  // reduxストアを利用できるようにする
  <Provider store={createStore(store)}>
    <App />
  </Provider>,
  document.getElementById('root')
)
App.js
import React from 'react'
import { connect } from 'react-redux'
import { inputTodo } from './actions/inputTodo'
import InputArea from './containers/inputArea'
class App extends React.Component {
  constructor(props) {
    super(props)
    this.submitTodo = this.submitTodo.bind(this)
  }
// TODOを追加し、入力欄を空にする
  submitTodo(contents, limitDate) {
    const contentsVal = contents.value
    const limitDateVal = limitDate.value
    if ('' === contents) return alert('予定を入力してください')
    if ('' === limitDate) return alert('日付を入力してください')
    contents.value = ''
    limitDate.value = ''
    // dispatchでAction Objが返ってくる
    this.props.dispatch(
      inputTodo({
        contents: contentsVal,
        limitDate: limitDateVal
      })
    )
  }
  render() {
    return (
      <div className="container">
        <h1>TODO List</h1>
        <ul className="todoList">
          {this.props.state.todoList.map(obj => {
            return (
              <li key={obj.id}>
                <input type="checkbox" className="todoCompBtn" />
                <span className="todoContents">{obj.contents}</span>
                <span className="todoLimitDate">{obj.limitDate}</span>
              </li>
            )
          })}
        </ul>
        <InputArea onClickBtn={this.submitTodo} />
      </div>
    )
  }
}
//reduxのstateをreactのコンポーネントに渡す
const selecter = state => {
  return {
    state: state.todoList
  }
}
// reactのコンポーネントをreduxのストアに接続する
export default connect(selecter)(App)
下の二つが何をしてるのか今一まだ理解しきれていません。
inputArea.js(App.jsから呼び出しているコンテナ)
import React from 'react'
class InputArea extends React.Component {
  render() {
    const { onClickBtn } = this.props
    return (
      <div className="todoInputArea">
        <label>
          内容<input type="text" ref="contents" className="InputTodoContents" />
        </label>
        <label>
          期限<input
            type="date"
            ref="limitDate"
            className="InputTodoLimitDate"
          />
        </label>
        <button
          type="button"
          className="inputBtn"
          onClick={() => onClickBtn(this.refs.contents, this.refs.limitDate)}
        >
          追加
        </button>
      </div>
    )
  }
}
export default InputArea
inputTodo.js(アクション)
export const INPUT_TODO = 'INPUT_TODO'
let todoId = 1
// Action Creator
export function inputTodo(obj) {
  // Action Objを返す
  return {
    type: INPUT_TODO,
    payload: obj,
    id: todoId++
  }
}
reducers/reducers.js
import { combineReducers } from 'redux'
import { INPUT_TODO } from '../actions/inputTodo'
const initialState = {
  todoList: []
}
const reducer = function(state = initialState, action) {
  switch (action.type) {
    case 'INPUT_TODO':
      //stateに対して更新をマージする
      return Object.assign({}, state, {
        todoList: [
          ...state.todoList,
          {
            contents: action.payload.contents,
            limitDate: action.payload.limitDate,
            id: action.id
          }
        ]
      })
    default:
      return state
  }
}
// アプリ内で持てるストアは一つだけ
// ので、combineReducersで(複数ある場合は)reducerを結合する
export const store = combineReducers({
  todoList: reducer
})
思ったよりもあっさりと動きました。
前回は「表示と処理が別のファイルにある」のが何となく気持ち悪かったのですが、
(上のコードはまだ分離し切れていませんが)そもそも別に書くものだ、と思えば却って見通しがすっきりしますね。
テストを書く
前回同様Jest+enzymeで書きます。
import React from 'react'
import Enzyme from 'enzyme'
import { shallow } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import InputArea from '../containers/inputArea'
Enzyme.configure({ adapter: new Adapter() })
describe('InputArea', () => {
  test('追加ボタン動作チェック', () => {
    let submitTodo = jest.fn()
    const subject = shallow(<InputArea onClickBtn={submitTodo} />)
    subject.find('.inputBtn').simulate('click')
    expect(submitTodo).toBeCalled()
  })
})
前回同様に、ボタンをクリックしたときに関数が呼ばれているかを確認します。
import { initialState, store } from '../reducers/reducers'
import { INPUT_TODO } from '../actions/inputTodo'
describe('reducers', () => {
  test('初期値チェック', () => {
    expect(store(initialState, {})).toEqual(initialState)
  })
  test('情報を入力', () => {
    expect(
      store(
        {},
        {
          type: INPUT_TODO,
          payload: {
            contents: 'いろはにほへと',
            limitDate: '2018-05-01'
          },
          id: 1
        }
      )
    ).toEqual({
      todoList: {
        todoList: [
          {
            contents: 'いろはにほへと',
            limitDate: '2018-05-01',
            id: 1
          }
        ]
      }
    })
  })
})
reducerのチェックです。
初期値が入っているかどうかと、渡した値に対して期待したものが返ってくるかを確認しています。
action creatorは正直書くべきなのかどうか今一分からなかったので今回は割愛しています。
参考:
ReactとReduxちょっと勉強したときの
https://qiita.com/mgoldchild/items/5be49ea49ebc2e4d9c55#storedispatch
Redux勉強用に簡単なサンプルを作ってみた
https://qiita.com/DJ_Middle/items/ffa08f983df471a5c6f8
Redux入門【ダイジェスト版】10分で理解するReduxの基礎
https://qiita.com/kiita312/items/49a1f03445b19cf407b7
React+Reduxのディレクトリ構成検討
https://qiita.com/bmf_san/items/58959501e6eae3609676
できあがったものがこちら
https://codesandbox.io/s/mj367lwxp
ここまで書いた処理の外に、チェックボックスをクリックしたら当該行を削除する処理も追加しています。
なお、reducerのテストが失敗してるのですが、テストから実行したときだけ何故かstateにinitialStateで渡した値ではなく[]が入っているようで、新しいstateを作るところでコケています。
三日くらい考えたんですが分からないので、いったんスルーしています……。
