React + Reduxで簡易掲示板

今回は、前回Reactで作った簡易掲示板をReduxでリプレイスします。


前回同様、↑のような簡易掲示板をReduxを使って作ります。

それでは、↓の手順にそって実装していきます。

  1. 必要なパッケージのインストール
  2. action作成
  3. reducer作成
  4. store作成
  5. component作成


必要なパッケージのインストール

Reduxで書き換えるに当たって↓のパッケージが必要なのでインストールします。

  • redux
  • react-redux
$ yarn add redux react-redux


また、今回作るアプリのディレクトリ構成は↓の通りです。

./src
├── actions
│   └── index.js
├── components
│   └── App.js
├── reducers
│   ├── comment.js
│   └── index.js
├── index.css
├── index.js
└── setupTests.js


action

はじめにactionを作っていきます。

actionは、storeに送られる情報を持つオブジェクトで、storeにアプリケーションの状態に変更を加える際の出発点になります。また、ActionはAction Creatorにより生成されます。

公式の定義

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().

https://redux.js.org/basics/actions

src/actions/index.jsを作成し、以下のように記述します。

src/actions/index.js

export const ADD_COMMENT = 'ADD_COMMENT';

// Action creator
export const addComment = (name, comment) => ({
  // Action シンプルなオブジェクト
  type: ADD_COMMENT,
  data: {
    name,
    comment
  }
})


Reducer

続いてreducer。

reducerは、現在のstateとactionから新しいstateを生成します。

(state, action) = newState

現在のstateに変更を加えるのではなく、actionの内容に応じて新しいstateを返す、というところがポイントです。

公式の定義

Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.

https://redux.js.org/basics/reducers

src/reducers/index.jsを用意し、以下のように記述します。

src/reducers/index.js

import { combineReducers } from 'redux';
// のちほど用意するsrc/reducers/comment.jsをインポート
import comment from './comment';

export default combineReducers({ comment });


combineReducers()とは

後ほど実装するstoreはcreateStore()メソッド によって生成されますが、このcreateStore()の引数に存在するreducerを全て渡す必要があります。

そして、createStore()に渡すreducerをひとまとめにしてくれるメソッドがcombineReducers()になります。

今回reducerはcommentの一つのみですが、combineReducers()を使ってみます。

続いて、src/reducers/comment.jsを用意し、以下のように記述します。

src/reducers/comment.js

import { ADD_COMMENT } from '../actions';

export default (state = {}, action) => {
  switch (action.type) {
    case ADD_COMMENT:
      const comments = state.commentList ? state.commentList : []
      return {
        commentList: [
          ...comments,
          {
            name: action.data.name === '' ? '名無しさん' : action.data.name,
            comment: action.data.comment,
          }
        ]
      }
    default:
      return state
  }
}


comment reducerは、現在のstateと新しいデータを持つactionを受け取り、それらを合成し新しいstateを返します。

Store

storeは、reducerとstateにアクセスできるオブジェクトです。

以下のような振る舞いをします。

  • アプリケーションの状態を持つ
  • getState()でstateにアクセスできる
  • dispatch(action)によりstateを更新できる

src/index.jsを以下のように記述します。

src/index.js

import { Provider } from 'react-redux';
import { createStore } from 'redux';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import * as serviceWorker from './serviceWorker';

import reducer from './reducers';

// combineReducers()でひとまとめにしたreducerを渡す
const store = createStore(reducer);

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

serviceWorker.unregister();


Provierとは

<App />を<Provider>で括ってますが、これには二つ役割があります。

connect()関数とは、コンポーネントとstoreを結びつける関数です。Providerで対象のコンポーネントをラップすることで、connect()を有効にし、かつstoreをコンポーネントに渡すことができるわけです。

Component

最後にコンポーネントの作成です。

create-react-appを作成した時にApp.jsはドキュメントルート配下にありますが、componentsディレクトリを作成し、その下に移動します。

src/components/App.jsを以下のように編集します。

src/components/App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

import { addComment } from '../actions';

class App extends Component {

  constructor() {
    super();
    this.state = {
      name: '',
      comment: '',
    };
  }

  handleInputChange = (e) => {
    switch (e.target.name) {
      case 'name':
        this.setState({
          name: e.target.value
        })
        break;
      case 'comment':
        this.setState({
          comment: e.target.value
        })
        break;
      default:
        break;
    }
  }

  send = (e) => {
    this.props.addComment(this.state.name, this.state.comment);
  }

  render() {
    const props = this.props;
    return (
      <Comments 
        commentList={props.commentList} 
        handleInputChange={this.handleInputChange} 
        send={this.send} 
      />
    )
  }
}

const Comments = (props) => (
  <div style={{width: 500, margin: 'auto'}}>
    <input name='name' type='text' placeholder='名前' onChange={props.handleInputChange}/><br />
    <textarea name='comment' style={{width: 400, height: 100}} onChange={props.handleInputChange} placeholder='何か書いてね'/>
    <input type='submit' value='追加' onClick={props.send}/>
    <table>
      <thead>
        <tr>
          <th>名前</th>
          <th>コメント</th>
        </tr>
      </thead>
      <tbody>
      {
        (props.commentList) &&
        props.commentList.map((comment, index) => (
          <tr key={index}>
            <td>{comment.name}</td>
            <td>{comment.comment}</td>
          </tr>
        ))
      }
      </tbody>
    </table>
  </div>
)


const mapStateToProps = state => {
  return ({ commentList: state.comment.commentList });
}

const mapDispatchToProps = ({ addComment: (name, comment) => addComment(name, comment) })

// ↑はこれのシンタックスシュガー
// const mapDispatchToProps = dispatch => {
//   return {
//     addComment(name, comment) {
//       dispatch(addComment(name, comment))
//     }
//   }
// }

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

先ほど出てきたconnect()はここで記述します。

connect()の第一引数、第二引数はそれぞれ mapStateToProps と mapDispatchToProps です。

mapStateToPropsとは

storeの持っているstateをpropsに反映しコンポーネントに渡す。ここではstateのcommentListがpropsに渡されます。

mapDispatchToPropsとは

dispatchを呼び出す関数をpropsに反映しコンポーネントに渡す。ここでは、addCommentがpropsに渡されます。


以上で準備ができました。アプリが起動するはずです。

$ yarn start


今回は以上になります。