「ReactでReduxを使ってみよう」の感想・備忘録1

スポンサーリンク

kindle本「ReactでReduxを使ってみよう」のまとめ。

点数

80点

感想

理解することはできたが、少々わかりづらい点が多かった。

感想としては、reduxはとにかく面倒くさい、特にredux-thunkを使うとソースが複雑になってしまう、という印象を受けた。

コンポーネントが増えるとstateの管理が大変になるので、大規模アプリではreduxを使っていきたい。

基礎

Reduxとは

ReduxはJavaScriptの状態管理ライブラリである。
状態管理ライブラリは、単純なアプリでは使わない方がよい。
多数のコンポーネント間で状態を共有したい場合にだけ使うべき。

  • 状態を変更したいコンポーネントはRedux Storeにdispatchを行う。
  • 状態を取得したいコンポーネントはRedux Storeにsubscribeでハンドラ登録を行う

Reactと相性が良いため、セットで紹介されていることが多いが、Redux自体は独立したものなので単体で利用することも可能。

用語

  • State
    Reduxアプリの状態。
  • Action
    Stateの変更に関する情報を持ったオブジェクト。
    typeプロパティは必須。
    payload(データ)をプロパティとして持つことができる。
  • Action Creator
    Actionを返す関数。
  • Store
    Stateを保持しているオブジェクト。
    Reduxアプリに1つだけ存在する。
    createStore(reducer)を使って作られる。

    以下のメソッドを持っている。
    dispatch(action):State変更
    getState():Stateの取得
    subscribe(listener):State変更時のコールバック関数の登録
  • Reducer
    Actionから新しいStoreを作成して返す関数。
Redux入門【ダイジェスト版】10分で理解するReduxの基礎 - Qiita
ReduxのGithubドキュメントを基に入門用記事として書いたものを、簡潔にまとめました。もと記事はこちらです。Redux入門 1日目 ReduxとはRedux入門 2日目 Reduxの基本…

Basic Example

Getting Started with Redux | Redux
Introduction > Getting Started: Resources to get started learning and using Redux
import { createStore } from 'redux'

const counter = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
};
let store = createStore(counter);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' }); // 1
store.dispatch({ type: 'INCREMENT' }); // 2
store.dispatch({ type: 'DECREMENT' }); // 1
  • State:dispatchメソッドの戻り値
  • Action:{ type: ‘INCREMENT’ }と{ type: ‘DECREMENT’ }
  • Action Creator:上記の例では使われていない
  • Store:変数store
  • Reducer:counter関数
    (第1引数が保持したいデータ。この例ではintだが、通常はオブジェクトにする)

処理の流れ

  • createStoreメソッドでStoreを作成。
    (ccounter関数が1度実行されstateが0になる)
  • store.subscribe()で状態変更時のハンドラを登録。
  • store.dispatch()で状態変更。引数がAction。

この例ではAction Creatorは使われていない。
Action Creatorを使う場合は、以下のようになる。

const increment = () => {
  return { type: 'INCREMENT' };
};
const decrement = () => {
  return { type: 'DECREMENT' };
};
store.dispatch(increment()) // 1
store.dispatch(decrement()) // 2
store.dispatch(increment()) // 1

3原則

Redux実装時に守るべき3つのルールがある。 https://redux.js.org/introduction/three-principles

  1. Stateは1つのStore内にオブジェクトツリーとして保持する
    cartとproductsの状態が必要な場合、state.cart, state.productsのようになる。
  2. Stateは読み取り専用でなければならない
    変更時はStoreのdispatchメソッドにActionを引数として渡す。
  3. Stateの変更は純粋関数(pure function)によって行われる
    (=Reducerは純粋関数出なければならない)

純粋関数とは

  • inputが同じであれば常に同じ結果を返す。
  • 副作用を生み出さない。
    (コンソールやディスクへの出力がない、参照渡しによる呼び出し元への影響がない)
    例えば、引数argで配列を受け取る関数の場合、 arg.push('hoge'); return arg;としている場合は非純粋関数、 return [...arg, 'hoge'];としている場合は非純粋関数。

Redux Devtools

chromeやfirefoxの拡張機能。
有効にするには、createStore関数に第2引数を渡す必要がある。

const store = createStore(
  counterReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
// または
import { createStore, compose } from 'redux'
const store = createStore(counterReducer, compose(window.devToolsExtension && window.devToolsExtension()));

redux-thunkなどのミドルウェアを使用する場合

import { createStore, applyMiddleware, compose } from 'redux';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(applyMiddleware(reduxThunk)));

Node.jsで実行

Node.jsでReduxを使ってみる

  1. npm install --save redux
  2. const { createStore } = require('redux');
    ※Node.js環境ではimportではなくrequireを使用する
  3. 下記コードをbasic1.jsとして保存
  4. node basic1.jsで実行
const { createStore } =  require('redux');

const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
};
let store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' }); // 1
store.dispatch({ type: 'INCREMENT' }); // 2
store.dispatch({ type: 'INCREMENT' }); // 3
store.dispatch({ type: 'DECREMENT' }); // 2

Stateをオブジェクトにしてみる

  1. 下記コードをbasic2.jsとして保存
  2. node basic2.jsで実行
const {createStore} = require('redux');

const userReducer = (state = {name: '', age: 0}, action) => {
  switch (action.type) {
    case 'SET_NAME':
      return {...state, name: action.name};
    case 'SET_AGE':
      return {...state, age: action.age};
    default:
      return state
  }
};

let store = createStore(userReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({type: 'SET_NAME', name: 'hoge'}); // { name: 'hoge', age: 0 }
store.dispatch({type: 'SET_AGE', age: 10}); // { name: 'hoge', age: 10 }

Reducerは、新しいオブジェクトにスプレッド構文を使用して純粋関数にしている。

Reactで実行

ReactでReduxを使ってみる

公式サイトにサンプルあり。
https://react-redux.js.org/introduction/quick-start

  1. npm install --save-dev create-react-app
  2. npx create-react-app basic1app
  3. cd basic1app; npm install redux react-redux
    react-redux:ReduxのStateの取得やActionのdispatchを、Reactコンポーネントからpropsオブジェクトを使って行うことができる。
  4. cd ./src; mkdir reducers; touch ./reducers/counterReducer.js
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
};
export default counterReducer;
  1. mkdir actions; touch ./actions/counterActions.js
export const increment = () => {
  return { type: 'INCREMENT' };
};
export const decrement = () => {
  return { type: 'DECREMENT' };
};
  1. index.jsを修正
// 〜省略〜
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import counterReducer from './reducers/counterReducer';
const store = createStore(
  counterReducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

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

ProviderでAppコンポーネントを囲む。
store属性にcreateStore関数で生成したStoreを渡す。
<Provider store={store}><App /></Provider>,

  1. App.jsを修正
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions/counterActions';

class App extends Component {
  render() {
    return (
      <div>
        <p>{this.props.count}回クリックされました。</p>
        <button onClick={this.props.increment}>+</button>
        <button onClick={this.props.decrement}>-</button>
      </div>
    );
  }
}
const mapStateToProps = (state) => {
  return {
    count: state
  }
};
const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement())
  }
};
export default connect(mapStateToProps, mapDispatchToProps)(App);

connect関数によりStateとdispatch関数をpropsにマッピングさせる。
・mapStateToProps:stateを受け取ってpropsにセットする関数
・mapDispatchToProps:dispatch関数を受け取ってpropsにセットする関数
※関数からreturnされたオブジェクトのキー名がprops名となる。
export default connect(mapStateToProps, mapDispatchToProps)(App);

mapStateToProps, mapDispatchToPropsの片方しか必要のないコンポーネントでは、connectは以下のようになる。
export default connect(mapStateToProps)(App);
export default connect(null, mapDispatchToProps)(App);

  1. npm startで実行

Stateをオブジェクトにしてみる

  1. npx create-react-app basic2app
  2. cd basic2app; npm install redux react-redux
  3. cd ./src; mkdir reducers; touch ./reducers/userReducer.js
const userReducer = (state = {name: '', age: 0}, action) => {
  switch (action.type) {
    case 'SET_NAME':
      return {...state, name: action.name};
    case 'SET_AGE':
      return {...state, age: action.age};
    default:
      return state
  }
};
export default userReducer;
  1. mkdir actions; touch ./actions/userActions.js
export const setName = (name) => {
  return { type: 'SET_NAME', name }; // name: nameの省略
};
export const setAge = (age) => {
  return { type: 'SET_AGE', age}; // ageはage: ageの省略
};
  1. index.jsを修正
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import userReducer from './reducers/userReducer';
const store = createStore(
  userReducer, 
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  1. App.jsを修正
import { setName, setAge } from './actions/userActions';

class App extends Component {
  handleSetNameClick = () => {
    this.props.setName('hoge');
  }
  handleSetAgeClick = () => {
    this.props.setAge(10);
  }
  render() {
    return (
      <div>
        <p>名前:{this.props.name}、年齢:{this.props.age}</p>
        <button onClick={this.handleSetNameClick}>Set Name</button>
        <button onClick={this.handleSetAgeClick}>Set Age</button>
      </div>
    );
  }
}
const mapStateToProps = (state) => {
  return {
    name: state.name,
    age: state.age
  }
};
const mapDispatchToProps = (dispatch) => {
  return {
    setName: (name) => dispatch(setName(name)),
    setAge: (age) => dispatch(setAge(age))
  }
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
  1. npm startで実行

コメント