クラスコンポーネントから乗り換えてみたいので勉強中。

[ webpack 4.39.2 / react 16.13.1 / react-dom 16.13.1 / redux 4.0.5 / react-redux 7.2.0 ]

ここでは↓こういうのを作る。

  1. めんどくさい人向け
  2. 必要なものインストール
  3. Webpack設定
  4. ソースコード (周辺)
  5. ソースコード (コンポーネント)
  6. 動かしてみる

めんどくさい人向け

GitHubに上げました。

git clone https://github.com/napoporitataso/react-redux-hooks-example-20200517.git
cd react-redux-hooks-example-20200517
npm install
npx webpack-dev-server

以下は手動で作成して動かす方法。

必要なものインストール

ここでは react-redux-hooks-example-20200517 ディレクトリ以下でやる。

mkdir react-redux-hooks-example-20200517
cd react-redux-hooks-example-20200517
npm init -y

必要なものをインストール。

npm install react react-dom redux react-redux
npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli webpack-dev-server

Webpack設定

プロジェクト直下に webpack.config.js を作成する。Babel と webpack-dev-server の設定だけ。

webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.jsx',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        query: {
          presets: ['@babel/preset-env', '@babel/preset-react']
        }
      }
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx']
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    host: 'localhost',
    port: 3000,
    open: true,
  },
};

ソースコード (周辺)

ざっと以下ファイルを用意する。本題にあまり関係無いけど必要。

dist/index.html
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
	<meta name="landscape" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
	<meta name="format-detection" content="telephone=no" />
	<title>サンプル</title>
</head>
<body>
	<div id="app"></div>
	<script type="text/javascript" src="./bundle.js"></script>
</body>
</html>
src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

import reducer from './reducers';
import Counter_class from './components/Counter_class';
import Counter_func from './components/Counter_func';
import Counter_hooks from './components/Counter_hooks';

const store = createStore(reducer());

ReactDOM.render(
  <Provider store={store}>
    <Counter_class />
    <Counter_func />
    <Counter_hooks />
  </Provider>,
  document.getElementById('app')
);
src/actions/index.js
export const COUNT_UP = 'COUNT_UP';
export const COUNT_DOWN = 'COUNT_DOWN';

export const countUp = (value) => {
  return {
    type: COUNT_UP,
    value
  };
}

export const countDown = (value) => {
  return {
    type: COUNT_DOWN,
    value
  };
}
src/reducers/index.js
import { combineReducers } from 'redux';
import counter from './counter';

const reducer = () => combineReducers({
  counter,
});

export default reducer;
src/reducers/counter.js
import { COUNT_UP, COUNT_DOWN } from '../actions';

const initialState = {
  clickCount: 0,
  currentValue: 0,
};

export default (state = initialState, action) => {
  switch (action.type) {
    case COUNT_UP:
      return {
        clickCount: state.clickCount + 1,
        currentValue: state.currentValue + action.value
      };
    case COUNT_DOWN:
      return {
        clickCount: state.clickCount + 1,
        currentValue: state.currentValue - action.value
      };
    default:
      return state;
  }
}

ソースコード (コンポーネント)

本題。以下の3つのコンポーネントは同じように動作する。

クラスコンポーネント

普通のクラスコンポーネント + mapStateToProps() + mapDispatchToProps()

src/components/Counter_class.jsx
import React from 'react';
import { connect } from 'react-redux';

import { countUp, countDown } from '../actions'

class Counter extends React.Component {
  handlePlusButton() {
    this.props.countUp(1);
  }

  handleMinusButton() {
    this.props.countDown(1);
  }

  render() {
    const { currentValue, clickCount } = this.props;
    return (
      <div>
        <h2>クラスコンポーネント</h2>
        <div>clickCount: {clickCount}</div>
        <div>
          <button onClick={(e) => { this.handleMinusButton(e); }}>-</button>
          <input type="text" value={currentValue} readOnly />
          <button onClick={(e) => { this.handlePlusButton(e); }}>+</button>
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    clickCount: state.counter.clickCount,
    currentValue: state.counter.currentValue,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    countUp: function (value) {
      return dispatch(countUp(value));
    },
    countDown: function (value) {
      return dispatch(countDown(value));
    },
  };
};

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

関数コンポーネント

単純にクラスコンポーネントを関数コンポーネントの書き方に変えただけ。引数で props を受け取る。

src/components/Counter_func.jsx
import React from 'react';
import { connect } from 'react-redux';

import { countUp, countDown } from '../actions'

const Counter = (props) => {
  const { currentValue, clickCount } = props;

  const handlePlusButton = () => {
    props.countUp(1);
  };

  const handleMinusButton = () => {
    props.countDown(1);
  };

  return (
    <div>
      <h2>関数コンポーネント</h2>
      <div>clickCount: {clickCount}</div>
      <div>
        <button onClick={(e) => { handleMinusButton(e); }}>-</button>
        <input type="text" value={currentValue} readOnly />
        <button onClick={(e) => { handlePlusButton(e); }}>+</button>
      </div>
    </div>
  );
};

const mapStateToProps = (state) => {
  return {
    clickCount: state.counter.clickCount,
    currentValue: state.counter.currentValue,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    countUp: function (value) {
      return dispatch(countUp(value));
    },
    countDown: function (value) {
      return dispatch(countDown(value));
    },
  };
};

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

関数コンポーネント + Hooks

関数コンポーネントではHooksを使ってすっきり書ける。

useSelector()useDispatch() は関数内にしか書けない。また、ここではSelectorの書き方はHooksを使わない場合の mapStateToProps() に寄せてみた。

src/components/Counter_hooks.jsx
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { countUp, countDown } from '../actions';

const Counter = () => {
  const { currentValue, clickCount } = useSelector(selector);
  const dispatch = useDispatch();

  const handlePlusButton = () => {
    dispatch(countUp(1));
  };

  const handleMinusButton = () => {
    dispatch(countDown(1));
  };

  return (
    <div>
      <h2>関数コンポーネント + Hooks</h2>
      <div>clickCount: {clickCount}</div>
      <div>
        <button onClick={(e) => { handleMinusButton(e); }}>-</button>
        <input type="text" value={currentValue} readOnly />
        <button onClick={(e) => { handlePlusButton(e); }}>+</button>
      </div>
    </div>
  );
};

const selector = state => {
  return {
    clickCount: state.counter.clickCount,
    currentValue: state.counter.currentValue,
  };
};

export default Counter;

動かしてみる

プロジェクト直下で以下コマンド。

npx webpack-dev-server

以上。