(´ワ`)

主に技術系備忘録、たまに日記。

いまさらReact + Reduxのセットアップをおさらいする

研究をろくにやらないでいるnnsです。

いろんなところでReactやReactNativeをさわる機会が多いものの覚えが悪いのでReact + ReduxのSPAプロジェクトの作成手順を毎回忘れます。

忘れないように自分なりに手順を書きます。あくまで手順メモなのでReactやReduxについての説明はほんのちょっぴりだけです。

1. Node.jsを入れる

そのまんま。バージョンはlatestを入れたほうがいいかも

2. create-react-appをグローバルでインストールする

npmを使用してReactプロジェクトをさらっと作ってくれるパッケージを入れます。

$ npm i -g create-react-app

-gオプションを加えることでグローバル環境下にパッケージを入れることができます。

3. create-react-appコマンドでReactプロジェクト作成

先ほど入れたパッケージを利用してプロジェクトを作成します。
コマンド名に続いてプロジェクト名を入力します。今回はsample-react-appにしました。

$ create-react-app sample-react-app

一応起動確認。勝手にブラウザにページが表示されると思います。

$ npm start

これでReactアプリケーションは作成されました。続いてReact-Reduxの環境を作ります。今回は最低限の環境を作るので数字のインクリメント・デクリメントをReduxで管理するアプリケーションを作成します。

4. Redux環境のインストール

作成したプロジェクトに対して新たに次のパッケージをインストールします。オプションに-S、あるいは--saveを入れることでプロジェクト環境にパッケージをインストールすることができます。

$ npm i -S react-redux redux

これでreduxに必要な2つのパッケージがインストールされました。

5. reducerの作成

依存関係のないものから作成するのがセオリーなのでreducerから作成します。
今回、管理したいstateは表示される数字、アクションはそれに対するデクリメント、インクリメントなのでそれらの実装をsrc/reducers/index.js配下に書いていきます。

5-1. Stateの初期状態の定義

すべてのStateは初期状態を持っている必要がありますのでこれを定義します。今回の場合、Stateである数字は0を初期値とします。

const initialState = 0
5-2. 状態を変更する関数を作成する

reducerは受け取ったアクションに対して新しい状態を返す役割を持ちます。今回の場合、デクリメント、あるいはインクリメントというアクションに対して状態である数字が変わるので、numという関数にします。
ちなみに、データフローにしたがって、初期起動時に必ずreducerは通るのでswitch文にdefaultを入れるのを忘れずにします。

function num(state = initialState, action) {
  switch(action.type) {
    case 'DECREMENT': {
      return state - 1;
    }
    case 'INCREMENT': {
      return state + 1;
    }
    default: {
      return state;
    }
  }
}

export default num;

6. Actionの作成

actionの役割はcomponentとreducerの橋渡し的なものです。component側から呼び出されたactionのタイミングに合わせてreducerが状態を変更してくれるという流れです。
実際のコードです。src/actions/index.js配下に書いていきます。

// action types
const DECREMENT = 'DECREMENT';

const INCREMENT = 'INCREMENT';

// action creators
function decrement() {
  return {
    type: DECREMENT,
  };
}

fucntion increment() {
  return {
    type: INCREMENT,
  };
}

actionにはreducer側でどのアクションが呼び出されたかを識別するためのActionTypeとcomponent側で呼び出すために使用するActionCreatorに分けられます。
変更されるStateに対して追加の情報を与えたいときはActionCreatorの関数に対して引数で渡します。そしてreturn文の中のオブジェクトに新しくその値をプロパティと添えて入れてあげるだけです。

7. Componentの作成

React-ReduxのComponentには大きく分けてContainer ComponentとPresentational Componentの2つがあります。
Container Componentから先に作ります。

7-1. Container Componentの作成

Container Componentはデータやコールバック関数などといったビジネスロジックをもったコンポーネントです。ですので実際の描画とはあまり関わることがほとんどありません。Stateを各Componentが持つPropsに変換するのもここで行います。 /src/container/AppContainer.jsに記述。/App.jsを一部書き換えます。

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

import App from '../component/App'
import logo from '../logo.svg';
import '../App.css';

class AppContainer extends React.Component {
  constructor(props) {
    super(props);
    this.decrement = this.decrement.bind(this);
    this.increment = this.increment.bind(this);
  }

  decrement() {
    alert('デクリメント');
  }

  increment() {
    alert('インクリメント');
  }

  render() {
    const { num } = this.props;
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        <div>
          <App num={num} decrement={this.decrement} increment={this.increment} />
        </div>
      </div>
    );
  }
}

function mapStateToProps(state) {
  return {
    num: state,
  };
}

export default connect(mapStateToProps)(AppContainer);

decrement()increment()は状態を変更するためのコールバック関数です。propsとしてComponentに渡すことでComponentのイベントリスナーに応じて反応します。
mapStateToProps()は関数名の通り、reduer(store)で管理しているStateをComponentで扱えるようにpropsとして変換しています。connect()はそのヒモ付を行ってくれる関数です。このファイルを呼び出すときにconnectされたものをデフォルトとしてインポートしてほしいのでconnectをexport defaultしています。

7-2. Presentational Componentの作成

Presentational Componentは描画のみを行うComponentです。ボタンを押したときの処理は後に記述するContainer Componentが担当するので、Presentational Componentはpropsから預かったコールバック関数(decrement()increment())を呼び出すだけです。
src/component/App.jsに記述します。

import React from 'react';

class App extends React.Component {
  render() {
    const { num, decrement, increment } = this.props;
    return (
      <div>
        <button onClick={() => decrement()}> - </button>
        <button onClick={() => increment()}> + </button>
        <div>{num}です</div>
      </div>
    );
  }
}

8. StoreをContainer Componentに注入

最後に、状態を持つreducer(Store)をContainer ComponentであるAppContainerに注入します。
/index.jsを以下のように変更

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import './index.css';
import App from './App';
import AppContainer from './container/AppContainer'
import registerServiceWorker from './registerServiceWorker';
import reducers from './reducers/'

const store = createStore(reducers);

ReactDOM.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>, 
  document.getElementById('root'),
);
registerServiceWorker();

createStore()の引数にreducerを代入することでstoreとして作成することができます。そしてContainer ComponentをProviderでラップ、storeというpropsにcreateStore()で作成したstoreを指定あげれば準備OK

9. Stateを変更できるかの確認

ここまでで一度起動すると、ボタン2コと「0です」というテキストが新たに作成された画面ができます。ボタンを押すとアラートが出るのみです。
ここでContainer Componentのコールバック関数内の内容を書き換えて実際にStateを変更します。
/src/container/AppContainer.jsを以下に変更

import { decrement, increment } from '../actions/'

....

  decrement() {
    const { dispatch } = this.props;
    dispatch(decrement());
  }

  increment() {
    const { dispatch } = this.props;
    dispatch(increment());
  }

....

Providerでラップされたコンポーネントにはdispatch()という関数が備わっています。これにActionCreatorを引数に与えてあげることでアクションがアタッチされ情報がreducerに送られます。

もう一度起動。無事インクリメント、デクリメントができました。

個人的にはこれに対してTypeScriptを導入したりすると、型の恩恵によりストレスフリーな開発ができるのでおすすめです。おしまい