banner
他山之石

他山之石

手を取り合ってシンプルなReduxを作ろう(二)

Redux の原理を理解することは、より良くそれを使用するのに役立ちます。本記事では react-redux の機能を実装します。

image

前回の記事では、簡単な Redux を実装し、主にその API を実装しました。本記事では、簡単な react-redux を実装します。

本文の完全なコードは Github でご覧ください:https://github.com/YanYuanFE/redux-app

// リポジトリをクローン
git clone https://github.com/YanYuanFE/redux-app.git


cd redux-app

// ブランチをチェックアウト
git checkout part-4

// インストール
npm install

// スタート
npm start

React でアプリケーションを開発する際、通常は props を使用してコンポーネント間でデータを渡しますが、アプリケーションのコンポーネント階層が非常に深くなった場合、ルートコンポーネントから最も内側のコンポーネントにデータを渡す必要がある場合、各レベルで手動で必要な props を渡す必要があります。この場合、React が提供する context API が必要です。

React 公式は context API の使用を推奨していません。なぜなら、context は実験的な API であり、将来の React バージョンで変更される可能性があるからです。現在まで、React 16 の最新バージョンでは context API が変更されています。

公式の警告にもかかわらず、context を使用する必要があるシナリオは依然として存在します。良い実践は、context のコードを小さな部分に隔離し、直接 context API を使用しないことです。これにより、将来的に API が変更されたときに、より簡単にアップグレードできます。これが react-redux のアプローチでもあります。

Context の使用法#

以下のコードを考えてみましょう:

const PropTypes = require('prop-types');

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = {
  color: PropTypes.string
};

class Message extends React.Component {
  render() {
    return (
      <div>
        {this.props.text} <Button>削除</Button>
      </div>
    );
  }
}

class MessageList extends React.Component {
  getChildContext() {
    return {color: "purple"};
  }

  render() {
    const children = this.props.messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = {
  color: PropTypes.string
};

上記のコードには 3 つのコンポーネントが含まれています。最上位のコンポーネント MessageList は複数の Message コンポーネントを含み、各 Message コンポーネントには Button コンポーネントが含まれています。最上位の MessageList コンポーネントから Button コンポーネントに color 属性を渡す必要がある場合、color 属性を手動で props を介して Message に渡し、次に Message から Button コンポーネントに渡す必要があります。上記のコードは Context API を使用してこれを実現しています。まず、context プロバイダーが必要で、ここでは MessageList です。MessageList コンポーネントは getChildContext メソッドと childContextTypes などの公式 API を追加する必要があります。getChildContext メソッドはグローバルな context オブジェクトを返すために使用され、childContextTypes は context 属性の型を定義します。React は自動的に context パラメータを下に渡します。任意のコンポーネントは、その子コンポーネント内で ContextTypes を定義することで context パラメータを取得できます。

Context の更新#

React 公式は現在、context の更新 API を廃止しています。context のデータを更新するには、this.setState を使用してローカル state を更新できます。state または props が更新されると、getChildContext が自動的に呼び出されます。新しい context が生成され、すべての子コンポーネントが更新を受け取ります。
以下のコードを考えてみましょう:

const PropTypes = require('prop-types');

class MediaQuery extends React.Component {
  constructor(props) {
    super(props);
    this.state = {type:'desktop'};
  }

  getChildContext() {
    return {type: this.state.type};
  }

  componentDidMount() {
    const checkMediaQuery = () => {
      const type = window.matchMedia("(min-width: 1025px)").matches ? 'desktop' : 'mobile';
      if (type !== this.state.type) {
        this.setState({type});
      }
    };

    window.addEventListener('resize', checkMediaQuery);
    checkMediaQuery();
  }

  render() {
    return this.props.children;
  }
}

MediaQuery.childContextTypes = {
  type: PropTypes.string
};

getChildContext では、this.state.type の値を持つ context オブジェクトを返します。context を更新する必要がある場合は、this.setState を呼び出して state を更新します。state が更新されると、getChildContext が自動的に実行され、新しい context が返されます。

react-redux の実装#

Provider の実装#

前回の記事では react-redux の使用について紹介しました。react-redux の API には主に connect と Provider が含まれています。まず、Provider の実装を見てみましょう。

Provider の使用法を振り返ります。

import { Provider } from 'react-redux';

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

上記のコードから、Provider はコンポーネントであり、アプリケーションのルートコンポーネントを包み、store の props を受け取ります。react-redux では、Provider コンポーネントが context を提供します。

前回の記事のプロジェクトを引き続き、src ディレクトリに react-redux.js を新たに作成し、まず Provider コンポーネントを宣言します。

import React from 'react';
import PropTypes from 'prop-types';

export class Provider extends React.Component {

}

Provider コンポーネントは独自の UI レンダリングロジックを持たず、context 部分のロジックを処理するだけです。

export class Provider extends React.Component {
  static childContextType = {
    store: PropTypes.object
  }
  constructor(props, context) {
    super(props, context)
    
    this.store = props.store

  }
  render() {

    return this.props.children

  }
}

このステップでは、静的メソッド childContextTypes で context 属性 store の型を object として定義し、constructor で props と context を受け取り、this.store を props.store に設定します。これにより、Provider 内のどこでも this.store を使用して props の store 属性にアクセスできます。
Provider は UI レンダリングを担当しないため、render メソッドでは this.props.children を直接返します。

最後に、Provider に getChildContext メソッドを追加して context を提供する必要があります。

export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext() {
    return {store: this.store}
  }
  constructor(props, context) {
    super(props, context)
    this.store = props.store
  }
  render() {
    return this.props.children
  }
}

getChildContext では context オブジェクトを生成します。この context は this.store であり、子コンポーネントが context を取得できるようにします。

connect の実装#

react-redux では、connect がコンポーネントを接続し、store の属性をコンポーネントの props に渡し、新しいコンポーネントを返します。このようなコンポーネント設計パターンは高階コンポーネントと呼ばれます。データが変更されると、connect はコンポーネントの更新を通知します。

connect の使用法を振り返ります。

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

const FilterLink = connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

connect はまず高階関数として定義されます。
react-redux.js で、まず connect メソッドを定義します:

export const connect = (mapStateToProps = state => state, mapDispatchToProps={}) => (WrapComponent) => {
    return class ConectComponent extends React.Component {
    }
}

connect は 2 層のアロー関数で、第一層では mapStateToProps と mapDispatchToProps パラメータを受け取ります。これらのパラメータはオプションであり、初期値を定義する必要があります。mapStateToProps は関数として定義され、mapDispatchToProps は関数またはオブジェクトのさまざまな形式を持つことができます。ここではデフォルトで空のオブジェクトとして設定します。connect メソッドは最終的にコンポーネントを返す必要があります。第二層の関数では、コンポーネントをパラメータとして受け取り、新しいコンポーネントを返します。上記のコードは以下のように書き換えることができます:

export function connect(mapStateToProps, mapDispatchToProps) {
  return function (wrapComponent) {
    return class ConnectComponent extends React.Component {

    }
  }
}

こうすると、非常に明確になります。connect は最初に最外層を実行して関数を返し、次にコンポーネントを渡し、最内層を実行して新しいコンポーネントを返します。
返されたコンポーネント内では、context を取得する必要があります。コードは以下の通りです:

static contextTypes = {
  store: PropTypes.object
}

次に constructor の実装です:

constructor(props, context) {
  super(props, context);
  this.state = {
    props: {}
  }
}

constructor 内では、props 属性を state として定義し、空のオブジェクトで初期化します。props は wrapComponent に渡されます。

render 関数内では:

render() {
  return <WrapComponent {...this.state.props}/>
}

render 関数内では、state.props を解構して WrapComponent の props 属性に渡します。もちろん、state.props はそれほど単純ではなく、mapStateToProps と mapDispatchToProps のデータを注入する必要があります。
componentDidMount 内のコードは以下の通りです:

componentDidMount() {
  const { store } = this.context;
  store.subscribe(() => this.update());
  this.update();
}

上記のコードでは、まず context から store を取得し、update メソッドを呼び出して state を更新します。同様に、store.subscribe に store 更新後のメソッドを渡し、store が更新されたときに update メソッドを呼び出す必要があります。update メソッドは以下の通りです:

update() {
  const { store } = this.context;
  const stateProps = mapStateToProps(store.getState());

  this.setState({
    props: {
      ...this.state.props,
      ...stateProps,
    }
  })
}

update メソッドでは、まず context から store を取得し、connect のデータを 2 つの部分に分けて考えます。第一部分は state のデータを props にマッピングするために、connect の最初のパラメータ mapStateToProps を呼び出し、store.getState () を渡します。つまり、グローバルな state を渡します。次に、stateProps オブジェクトを取得して props に渡します。そして、this.setState を使用して state を更新します。ここではオブジェクトの展開構文を使用して新旧 state をマージします。

上記では state データのマッピングのみを実装しましたが、メソッドのマッピングも必要です。データのマッピングは比較的簡単ですが、メソッドは直接使用できません。なぜなら、メソッド呼び出しには store.dispatch が必要だからです。ここで、redux 内で bindActionCreators メソッドを実装し、以下のように呼び出します:

import { bindActionCreators } from './redux';

const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

bindActionCreators は store.dispatch を関数内に渡し、関数を呼び出すと内部でその関数を dispatch できるようにします。

src ディレクトリの redux.js で bindActionCreators メソッドを実装します:

export function bindActionCreators(creators, dispatch) {
  let bound = {};
  Object.keys(creators).forEach(v => {
    let creator = creators[v];
    bound[v] = bindActionCreator(creator, dispatch);
  })
  return bound;
}

bindActionCreators メソッドでは、connect の第二パラメータ mapDispatchToProps を creators として受け取ります。creators はオブジェクトであり、ここでは creators 内の各メソッドを dispatch でラップする必要があります。Object.keys を使用してオブジェクトの可列挙プロパティからなる配列を返し、配列をループして、配列のインデックスに対応するメソッドを取得し、bindActionCreator を呼び出して dispatch でラップされた新しいメソッドを返し、新しいオブジェクト bound に組み立てて返します。key はそのまま保持します。
次に bindActionCreator を実装します:

function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args))
}

bindActionCreator メソッドでは、高階関数を使用して新しい関数を返します。元の関数 creator は dispatch でラップされ、残りの引数...args はラップされた関数内に透過されます。これは、引数が最内層に渡されることを保証するためです。
ここまでで、connect の 2 つの部分のデータが実装されました。以下は完全な update メソッドです:

update() {
  const { store } = this.context;
  const stateProps = mapStateToProps(store.getState());

  const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

  this.setState({
    props: {
      ...this.state.props,
      ...stateProps,
      ...dispatchProps,
    }
  })
}

最終的な update メソッドでは、stateProps と dispatchProps を state.props に更新します。これにより、データが更新されるたびに子コンポーネントに通知され、同期更新が行われます。
これで、react-redux の基本機能が実装されました。以下は完全な react-redux コードです:

import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from './redux';

export const connect = (mapStateToProps = state => state, mapDispatchToProps={}) => (WrapComponent) => {
  return class ConectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    }

    constructor(props, context) {
      super(props, context);
      this.state = {
        props: {}
      }
    }
    componentDidMount() {
      const { store } = this.context;
      store.subscribe(() => this.update());
      this.update();
    }
    update() {
      const { store } = this.context;

      const stateProps = mapStateToProps(store.getState());

      const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);

      this.setState({
        props: {
          ...this.state.props,
          ...stateProps,
          ...dispatchProps,
        }
      })
    }
    render() {
      return <WrapComponent {...this.state.props}/>
    }
  }
}

export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  }
  getChildContext() {
    return {store: this.store}
  }
  constructor(props, context) {
    super(props, context);
    this.store = props.store;
  }
  render() {
    return this.props.children
  }
}

react-redux を使用してカウンターアプリを改写する#

以前の記事では、自分で作成した redux を使用して簡単なカウンターアプリを実装しました。これを react-redux を使用して改写します:

App.jsは以下の通りです:

import React, { Component } from 'react';
import { connect } from './react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    const { onIncrement, onDecrement, counter } = this.props;

    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Reactへようこそ</h1>
        </header>
        <p className="App-intro">
          始めるには、<code>src/App.js</code>を編集して保存してください。
        </p>
        <Counter
          value={counter}
          onIncrement={onIncrement}
          onDecrement={onDecrement}
        />
      </div>
    );
  }
}

const mapStateToProps = (state) => ({
  counter: state
});

function onIncrement() {
  return { type: 'INCREMENT' }
}

function onDecrement() {
  return { type: 'DECREMENT' }
}

const mapDispatchToProps = {
  onIncrement,
  onDecrement
};

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

注意:react-redux の connect 内で mapDispatchToProps の処理は、値がオブジェクトである場合のみ考慮されているため、実際にはオブジェクトまたは関数をパラメータとしてサポートする必要があります。したがって、ここではオブジェクトの形式として設定する必要があります。

index.js のコードは以下の通りです:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from './react-redux';
import './index.css';
import App from './App';
import { createStore } from './redux';
import counter from './reducers';

const store = createStore(counter);

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

最後に、npm start でプロジェクトを実行し、ブラウザのインターフェースを開くと、以下のように操作し、期待通りの効果が得られます:

image

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。