Hirooooo’s Labo

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

React × Redux 初心者入門(1日目:webpackで環境構築編) ver.2018

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

1年半前に同じ記事を書いたのですが、バージョンアップによる変更が多く、古い記事ではまともに動かすこともできない記事となってしまったため、React × Reduxの連載記事を1日目から再投稿していこうと思います。 記事の文言等々は過去の記事をリライトしてますので、ご了承ください。

それではReactReduxを使用してWebアプリを構築していきたいと思います。

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

使用プラグイン、ライブラリ等は追々検討するということで。。
react-routerとか、axiosとか、material-uiとか。

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

今の私の環境

  • Mac OS X High Sierra ver 10.13.2

React環境構築

HomeBrewのインストール

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

www.hirooooo-lab.com

Node.jsのインストール

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

www.hirooooo-lab.com

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

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

$ mkdir god
$ cd god

※ 私の場合はgod (笑)

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

 $ 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>` afterwards to install a package and
save it as a dependency in the package.json file.

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

{
  "name": "god",
  "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
    • prop-types
    • redux
    • react-redux
  • devDependencies
    • babel-cli
    • babel-core
    • babel-eslint
    • babel-loader
    • babel-preset-env
    • babel-preset-react
    • babel-preset-stage-0
    • eslint
    • eslint-loader
    • eslint-plugin-react
    • file-loader
    • html-webpack-plugin
    • webpack
    • webpack-dev-server

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

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

$ npm install --save-dev babel-cli babel-core babel-eslint babel-loader babel-preset-env babel-preset-react babel-preset-stage-0 eslint eslint-loader eslint-plugin-react file-loader html-webpack-plugin webpack webpack-dev-server

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

  "dependencies": {
    "prop-types": "^15.6.0",
    "react": "^16.2.0",
    "react-dom": "^16.2.0",
    "react-redux": "^5.0.6",
    "redux": "^3.7.2"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-eslint": "^8.2.1",
    "babel-loader": "^7.1.2",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "eslint": "^4.15.0",
    "eslint-loader": "^1.9.0",
    "eslint-plugin-react": "^7.5.1",
    "file-loader": "^1.1.6",
    "html-webpack-plugin": "^2.30.1",
    "webpack": "^3.10.0",
    "webpack-dev-server": "^2.11.0"
  }

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

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

設定ファイルの作成

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

.babelrcの追加

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

{
  "presets": [
    [
      "env",
      {
        "targets": {
          "browsers": [
            "last 2 versions",
            "safari >= 7"
          ]
        }
      }
    ],
    "react",
    "stage-0"
  ]
}

.eslintrcの追加

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

{
  "env": {
    "es6": true,
    "browser": true,
    "node": true
  },
  "rules": {},
  "parser": "babel-eslint",
  "plugins": [
    "react"
  ],
  "ecmaFeatures": {
    "arrowFunctions": true,
    "jsx": true
  }
}

webpack.config.jsの作成

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

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  context: __dirname,
  entry: [
    './src/index.jsx',
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      { test: /\.jsx?$/, enforce: 'pre', exclude: /node_modules/, loader: 'eslint-loader' },
      { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel-loader'] },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      inject: 'body',
    }),
    new webpack.LoaderOptionsPlugin({
      options: {
        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 --history-api-fallback",
    "build": "webpack --progress --colors"
  },

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

Reactプロジェクト構成

最終的にはこんな感じを目指します。
f:id:hirooooo-lab:20180117123411p: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';

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

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

const CounterActions = {
  increment,
  decrement,
};

export default CounterActions;

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 * as types from '../constants/ActionTypes';

const initialState = {
  value: 0,
};

export default function counter(state = initialState, action) {
  switch (action.type) {
    case types.INCREMENT:
      return { value: state.value + 1 };
    case types.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 } from 'react';
import PropTypes from 'prop-types';
import Header from '../components/Header';

class App extends Component {

  static propTypes = {
    counter: PropTypes.object.isRequired,
    actions: PropTypes.object.isRequired,
  };

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

次にactionとstateにおいて、reactとreduxを連結させます。
以下のソースを追記していきます。

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

import * as CounterActions from '../actions/counter';

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

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Object.assign({}, CounterActions), dispatch),
  };
}

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

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

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

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import Header from '../components/Header';
import CounterActions from '../actions/counter';

class App extends Component {

  static propTypes = {
    counter: PropTypes.object.isRequired,
    actions: PropTypes.object.isRequired,
  };

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

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

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Object.assign({}, CounterActions), dispatch),
  };
}

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

components/Header.jsx

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

import React, { 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,
  );

  return store;
}

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

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

src/index.jsx

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

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

const store = configureStore();

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>React-Redux Boilerplate</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

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

実行してみる

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

$ npm start

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

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

まとめ

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

以前の古い記事はこちら

www.hirooooo-lab.com

次回予告

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

  • 開発環境の効率化をするためにHotReloadとソースマップを実装する
  • ESLintでAirbnbのスタイルガイドを実装する
  • MaterialデザインとCSSを実装する
  • ページルーティングを実装する
  • REST通信を実装する
  • ログイン制御のフローを実装する
  • デプロイ方法を検討、実装する

次の記事

www.hirooooo-lab.com