Hirooooo’s Labo

開発メモ、ガジェット、日記、趣味など、思った事を思ったまんま書くブログ

React × Redux 初心者入門(1日目:環境構築編)

f:id:hirooooo-lab:20160713002241j:plain

今度仕事でReact使う事になったので、勉強のためReact-Reduxを使ってWebアプリを作成してみます。
使用プラグイン、ライブラリ等は追々検討するということで。。
react-routerとか、axisとか、material-uiとか。

まずはサラのMacに環境構築から、react-reduxのsampleが動くところまでやってみたいと思います。

基本的に自分メモなので、よくわかってないで書いてる部分もすごいあるので、間違いとかあったらゴメンナサイ。

今の私の環境

  • Mac OS X El Capitan ver 10.11.5

環境構築

HomeBrewのインストール

Macのパッケージマネージャ「HomeBrew」のインストールについては過去にメモを書いたのでそちら参照。

www.hirooooo-lab.com

Node.jsのインストール

Node.jsをインストールしてnpmを使えるようにします。 こちらも過去にメモを書いたのでそちら参照。

www.hirooooo-lab.com

開発ディレクトリを作成して初期化する

適当なアプリのソースディレクトリを作成して移動します。

$ mkdir angel
$ cd angel

※ 私の場合はangel (笑)

続いてパッケージの初期化を行います。

 $ npm init

いろいろ聞かれるので、良しなに答えておけばOK。
基本Enter連発でも大丈夫。

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (angel)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/xxxxxxxx/Dev/angel/package.json:

{
  "name": "angel",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Is this ok? (yes)

これで配下にpackage.jsonができていればOKです。

必要なNodeModuleを取得

とりあえず今回使うModuleは以下です。

  • dependencies
    • react
    • react-dom
    • redux
    • react-redux
  • devDependencies
    • webpack
    • webpack-dev-server
    • babel-cli
    • babel-core
    • babel-loader
    • react-hot-loader
    • eslint
    • eslint-loader
    • eslint-plugin-react
    • babel-eslint
    • file-loader
    • babel-preset-es2015
    • babel-preset-react
    • css-loader
    • style-loader

この辺りは今後ちゃんと検討していくと思うので、とりあえずは下記コマンドで取り込んじゃいましょう。

$ npm install --save react react-dom redux react-redux

$ npm install --save-dev webpack webpack-dev-server babel-cli babel-core babel-loader react-hot-loader eslint eslint-loader eslint-plugin-react babel-eslint file-loader babel-preset-es2015 babel-preset-react css-loader style-loader

package.jsonの以下のdependenciesが追加されたと思います。

  "dependencies": {
    "react": "^15.2.1",
    "react-dom": "^15.2.1",
    "react-redux": "^4.4.5",
    "redux": "^3.5.2"
  },
  "devDependencies": {
    "babel-cli": "^6.10.1",
    "babel-core": "^6.10.4",
    "babel-eslint": "^6.1.2",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.11.1",
    "css-loader": "^0.23.1",
    "eslint": "^3.0.1",
    "eslint-loader": "^1.4.1",
    "eslint-plugin-react": "^5.2.2",
    "file-loader": "^0.9.0",
    "react-hot-loader": "^1.3.0",
    "style-loader": "^0.13.1",
    "webpack": "^1.13.1",
    "webpack-dev-server": "^1.14.1"
  }

とりあえずこれで必要な基盤準備は整いました。

続いて設定ファイルを追加していきます。

設定ファイルの作成

babelの設定ファイル、eslantの設定ファイル、webpackの設定ファイルを作成する。
これらについてはまだまだ勉強が必要ですが、とりあえず動くところまで持っていくために、今回は下記のファイルをそのまま置いておけばOKです。

.babelrcの追加

BABELの設定ファイルを追加します。
記載内容については今回は触れません。
.babelrcファイルを作成し、以下のコード追加してください。

{
  "presets": ["es2015", "react"]
}

.eslintrcの追加

ESLintの設定ファイルを追加します。
例によって記載内容については今回は触れません。
.eslintrcファイルを作成し、以下のコードを追加してください。

{
  "env": {
    "es6": true,
    "browser": true,
    "node": true
  },
  "rules": {
    "curly": 0,
    "comma-dangle": [2, "never"],
    "comma-spacing": 0,
    "eqeqeq": [2, "allow-null"],
    "key-spacing": 0,
    "no-underscore-dangle": 0,
    "no-unused-expressions": 0,
    "no-shadow": 0,
    "no-shadow-restricted-names": 0,
    "no-extend-native": 0,
    "no-var": 2,
    "new-cap": 0,
    "quotes": 0,
    "semi-spacing": 0,
    "space-unary-ops": 0,
    "space-infix-ops": 0,
    "consistent-return": 0,
    "strict": 0
  },
  "parser": "babel-eslint",
  "plugins": [
    "react"
  ],
  "ecmaFeatures": {
    "arrowFunctions": true,
    "jsx": true
  }
}

webpack.config.jsの作成

webpackの設定ファイルを作成します。 こちらも記載内容は省きますが、ちゃんと理解しないといけないなぁ。。と思っております。 webpack.config.jsファイルを作成して、以下のコードを追加してください。

module.exports = {
  context: __dirname,
  entry: {
    jsx: "./src/index.jsx",
    css: "./src/main.css",
    html: "./src/index.html",
  },

  output: {
    path: __dirname + "/static",
    filename: "bundle.js",
  },
  module: {
    preLoaders: [
        //Eslint loader
      { test: /\.jsx?$/, exclude: /node_modules/, loader: "eslint-loader"},
    ],
    loaders: [
      { test: /\.html$/, loader: "file?name=[name].[ext]" },
      { test: /\.css$/, loader: "file?name=[name].[ext]" },
      { test: /\.jsx?$/, exclude: /node_modules/, loaders: ["react-hot","babel"]},
    ],
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  eslint: {
    configFile: './.eslintrc'
  },
};

package.jsonのscriptを書き換える

起動するためのnpm scriptを追加します。 npmによって作成されたpackage.jsonscript要素を以下のように変更します。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --hot --inline --progress --colors",
    "build": "webpack --progress --colors"
},

これで起動までの設定は完了です。 つぎはいよいよソースを追加していきます。

プロジェクト構成

最終的にはこんな感じを目指します。
f:id:hirooooo-lab:20160712121832p:plain

カウンターアプリを作ってみる

とりあえず、exsanpleなんかで扱われてるカウンターアプリを作ってみます。

Actionの作成

Actionのタイプを定義したActionTypes.jsxとactionのfunctionを定義したcounter.jsxを作成します。

constants/ActionTypes.jsx

constants/ActionTypes.jsxを作成し、インクリメントとデクリメントのアクションタイプを定義します。

export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

actions/counter.jsx

actions/counter.jsxを作成し、incrementアクションとdecrementアクションを作成します。
ActionTypesをimportしてtype定義を返しているだけです。

import * as types from '../constants/ActionTypes'

export function increment() {
  return { type: types.INCREMENT }
}

export function decrement() {
  return { type: types.DECREMENT }
}

Reducerの作成

続いてReducerの作成です。 Reducerは現在のstateとAction から、新しいstateを生成するfunctionです。
現在のstateを更新してはだめで、新しいstateを返さないとダメだそうです。

reducers/counter.jsx

reducers/counter.jsxを作成し、actionTypeにそった処理を実装します。
今回はstateにvalueという数値を持っているだけなので、incrementで+1をdecrementで-1をstateのvalueに行い、その結果を新しいstateとしてreturnしてます。 すべてのactionはreducerに渡されてしまうので、未定義のactionが来た場合はstateをそのまま返すようにします。
initialStateはstateの初期値を設定してます。

import { INCREMENT, DECREMENT } from '../constants/ActionTypes'

const initialState = {
  value: 0
}

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

reducers/index.jsx

今はまだcounter.jsxだけですが、今後複数のreducerができた場合のために、combineReducersを使ってrootReducerにまとめておきます。

import { combineReducers } from 'redux'
import counter from './counter'

const rootReducer = combineReducers({
    counter
})

export default rootReducer

これでreducerの準備は完了

container、componentsの作成

reactのcomponentを作成します。 ちなみに、containerもcomponentなんですが、reduxに関与する一番上のレイヤーのコンポーネントをcontainerに、reactオンリのコンポーネントをcomponentに作成します。
詳しくはこの辺りの記事が参考になりました

qiita.com

containers/App.jsx

メインコンテナとなるcontainers/App.jsxを作成します。
まずはrenderを作成しcount={value}でstateのvalueを表示します。
その下にボタンを2つ用意し、actionのincrementとdecrementをonClickに設定します。
<Header />は後述で作成するコンポーネントです。

import React, { Component, PropTypes } from "react"
import Header from '../components/Header'

class App extends Component {
  render() {
    const { value, actions } = this.props;
    return (
      <div>
        <Header />
        <h2>count={value}</h2>
        <button onClick={actions.increment}>増加</button>
        <button onClick={actions.decrement}>減少</button>
      </div>
    )
  }
}

次にactionとstateにおいて、reactとreduxを連結させます。

import React, { Component, PropTypes } from "react"
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
// Appコンポーネントにstateを流し込む
function mapStateToProps(state) {
  return state.counter
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(CounterActions, dispatch)
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

function mapStateToProps(state)でstateをコンポーネントのpropsに流し込みます。
function mapDispatchToProps(dispatch)でactionとdispatchをひも付けてます。
最後にconnectを使って、reactComponentとひも付け完了のようです。
※正直良くわかってないですが、こんなイメージでやってます。。。

全体的にはこうなります。

import React, { Component, PropTypes } from "react"
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Header from '../components/Header'
import * as CounterActions from '../actions/counter'

class App extends Component {
  render() {
    const { value, actions } = this.props;
    return (
      <div>
        <Header />
        <h2>count={value}</h2>
        <button onClick={actions.increment}>増加</button>
        <button onClick={actions.decrement}>減少</button>
      </div>
    )
  }
}

App.propTypes = {
  value: PropTypes.number.isRequired,
  actions: PropTypes.object.isRequired
}

// Appコンポーネントにstateを流し込む
function mapStateToProps(state) {
  return state.counter
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(CounterActions, dispatch)
  }
}
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

components/Header.jsx

App.jsxだけじゃ味気ないので、components/Header.jsxを作成してヘッダー部分のコンポーネントを作成しておきます。
ただし、propsは受けないベタ書きです。

import React, { PropTypes, Component } from 'react'

class Header extends Component {
  render() {
    return (
      <header className="header">
        <h1 >ヘッダだYo</h1>
      </header>
    )
  }
}
export default Header

これは先程のcontainers/App.jsxに読み込まれます。

storeの作成

Storeはアプリケーションの状態(state)を一つだけ保持しています。 storeを作成する際には、reducerを渡して登録します。 dispatchすることによって、reducerに現在のstateとactionを渡して、結果のstoreを作成します。

store/configureStore.jsx

store/configureStore.jsxを作成し、createStoreで作成したstoreを返します。

import { createStore } from 'redux'
import rootReducer from '../reducers'

export default function configureStore(initialState) {
  const store = createStore(
    rootReducer,
    initialState
  )

  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextReducer = require('../reducers')
      store.replaceReducer(nextReducer)
    })
  }

  return store
}

if (module.hot) {はwebpackのhotModuleで使用されるみたいですが、よくわかってないのでおいておきます。。(汗)

エントリポイントのindexの作成

最後にエントリポイントとなるindex.jsxとindex.htmlを作成します。

src/index.jsx

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

import App from '../containers/App'
import configureStore from '../store/configureStore'

// 作成したreducerであるcounter関数を引数に指定してstoreを作成
const store = configureStore()

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
)

Provider store={store}>によって、配下でstoreを使用できるようにしています。 今回のソースはproviderで内にAppのコンテナ含めて、それをhtmlのroot属性のところに注入しています。

src/index.html

注入されるrootのidのdivタグと、生成されたbundle.jsを読み込んでいるだけです。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Boilerplate</title>
    <link rel="stylesheet" type="text/css" href="/main.css">
  </head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>

おまけでcssも読み込んでいるので、src/main.cssも作っておきましょう。

html {
  font-family: 'Roboto', sans-serif;
}

body {
  font-size: 13px;
  line-height: 20px;
  margin: 0px;
}

これで一通りの作成が終わりました。

実行してみる

ターミナルから以下のnpm startを実行してみてください。

$ npm start

正常にビルドが通ったらhttp://localhost:8080/にアクセスるとこんなページが表示されると思います。

f:id:hirooooo-lab:20160712153133p:plain

増加ボタンと減少ボタンでaction が呼ばれ、stateのvalueが変更され、画面のcountの値が変わると思います。

まとめ

とりあえずreact,reduxを使ったすご~く簡単な画面ができました。 まだまだ勉強することが山積みですが、これから頑張って習得して行きたいと思います。

参考にさせていただいたサイト

チュートリアル | React

ES6版React.jsチュートリアル - Qiita

React + Redux入門初歩 実際に一からアプリを構築してみる - Qiita

ReduxとES6でReact.jsのチュートリアルの写経 - undefined

Reduxチュートリアル動画を見ながらReduxを基礎の基礎から - Qiita

Redux入門【ダイジェスト版】10分で理解するReduxの基礎 - Qiita

何をreduxのコンテナにするか - Qiita

Material UIを使ってカッコいいUIのReactアプリケーションを作ってみた – PAYFORWARD

次回予告

次回予告というか、あとやらなきゃいけないと思ってること

  • Materialデザインを実装する
  • ページルーティングを実装する
  • REST通信を実装する
  • ログイン制御のフローを実装する
  • デプロイ方法を検討、実装する

次の記事

www.hirooooo-lab.com