EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

React+Reduxについて

最近、React+Reduxでコンポーネント実装に取り組んでいるのですが、今回はそれを通して知ったことをブログにまとめたいと思います。

Reduxとは

Reactでは、stateという仕組みを通してコンポーネントとデータ(状態)の双方向バインディングを実現しています。通常のstateは単独のコンポーネントで使用するか、propsを通して他のコンポーネントへ渡す必要がありますが、複数のコンポーネントで協調的にstateを使うために、全体のstate(Storeと呼びます)を一元管理する仕組みがReduxになります。

Reduxの仕組み

Reduxでは、複数コンポーネント(アプリケーション)で共有するstateを保持するStoreと、Storeを更新するためのReducer、そしてReducerに対して、どのようにStoreを更新するかを伝えるActionで構成されています。

ユーザー操作で値が変更されるUIは、

  1. UI(VIewComponent)にReduxの処理をconnectする
  2. UIのイベントをハンドリングする
  3. イベントの種類と更新値をセットにしたActionを生成する(Action create)
  4. ActionをReducerに渡す(dispatch)
  5. ReducerでStoreの値を更新する(reduce)
  6. 値の更新がUIに反映される

という流れで実現されます。 この構成(デザインパターン?)は理解しやすかったのですが、次に紹介する実際の使い方を把握するまで苦労しました…。

Reduxの使い方

まあ、使い方と言っても、ほぼReduxのTutorial Basic Tutorial: Intro · Redux のままなんですが。

Action(とActionCreator)

Actionとは、イベントの種類と値をセットにしたものです。

{
  type: SUBMIT,
  value
};

実際は、以下のようにイベントに応じてActionを生成する、ActionCreatorという関数を用意します。

const SUBMIT_PARENT_A = "SUBMIT_PARENT_A";
export const submit = (value) => {
  return {
    type: SUBMIT,
    value
  };
};

Reducer

Reducerは、Actionに応じてStoreの値を更新する処理です。 インターフェースとしては、現在のstateとActionを受け取り、新しいstateを返します。 Reduxでは、Reducerを通してStoreを更新することでUIが再描画され、双方向バインディングが実現されています。

Reduxでは初めにStoreを生成する際に空のreducerを呼び出しstateを取得するようなので、引数のstateのデフォルト値を初期値にしてそのまま返すことで、生成時の初期値を設定するのが常套のようです。

const initialState = {
  name: 'initial name',
  content: 'initial content',
};

export const reducer = (state = initialState, action) => {
  switch (action.type) {
    case SUBMIT:
      return Object.assign({},state,{
        name: action.value.name,
        content: action.value.content,
      });
    default:
      return state;
  };
}

Store

で、上のReducerを渡してStoreを生成します。

const store = createStore(reducer);

アプリケーションが大きくなるとStoreを構成するオブジェクトの構造が複雑になりますが、Reducerを複数の関数に分割して、更新するstateだけを処理することができます。

例えば、userに関する処理を行うReducerをuserReducer、foodに関する処理を行うReducerをfoodReducerと定義した場合、

const store = createStore(
  combineReducers({
    user: userReducer,
    food: foodReducer
  })
);

とすると、userReducerのstateにはstore.user以下のstate、foodReducerのstateにはstore.food以下のstateを受け取ることができます。

そして以下のように、生成したStoreをアプリケーションと紐づけます。

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

connect

コンポーネントからReduxのStoreにアクセスしたりActionを介して値を更新するために、connectする必要があります。

connectには、2つの関数を渡すことで、

  • Storeの値をpropsに設定(mapStateToProps)
  • ActionをReducerに渡す(dispatchする)関数をpropsに設定(mapDispatchToProps)

ができます。

mapStateToPropsは、state(Storeに格納されているstate全体)を引数に受け取るので、以下のように必要な値をpropsに設定します。

const mapStateToProps = (state) => {
  return {
    name: state.name,
    content: state.content,
  }
}

mapDispatchToPropsは、ReducerにActionを渡すdispatch関数を引数に受け取るので、以下のように関数をpropsに設定します。

const mapDispatchToProps = (dispatch) => {
  return {
    onChange: (e) => {
      dispatch(submit({
        name: e.target.name,
        content: e.target.value,
      }))
    },
  }
}

そして、これらをコンポーネントにconnectします。

class TestComponent extends Component<Props> {
  public render() {
    const {name,content,onChange} = this.props;
    return (
      <div>
        {name}:{content}
          <select name="area" onChange={(e)=>onChange(e)>
          <option value="北海道">北海道</option>
          <option value="青森県">青森県</option>
          <option value="岩手県">岩手県</option>
          <option value="秋田県">秋田県</option>
          <option value="宮城県">宮城県</option>
          ・・・
          </select>
      </div>
    );
  }
}

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

これによって、コンポーネントにStoreの値を渡し、値を変更できるようになりました。

Reduxを使ってみて

そもそもReactに触れるのもはじめてだったので理解して慣れるまで苦労しましたが、分かってくると案外シンプルで色々と使えるので便利だと感じました。

デバッグについて

chromeで開発しているのですが、こちらの拡張機能がとても役に立ちました。

GitHub - zalmoxisus/redux-devtools-extension: Redux DevTools extension.

拡張機能をインストール後に、createStore時に

const store = createStore(
  reducer, 
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
 );

thunxやrouterなどのmiddlewareを使う場合は

const composeEnhancers = window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__'] as typeof compose || compose;
const store = createStore(
  reducer,
  composeEnhancers(applyMiddleware(・・・))
);

とする必要があります。

アプリを起動後に開発者ツールのreduxで、dispatchされたActionやStoreの中身を見ることができます。

全体のStateを取得する場合

今回のアプリケーションでは、画面の種類に応じてReducerを作りcombineReducersで結合することで、自画面に関係無いstateを意識しないように設計していますが、最終的にStore全体にわたるデータをサーバーに送信するために、以下を参考にしました。

Reducers · Redux
ReduxのFAQを読み直す - Qiita

具体的には、今回はサーバーとの通信にredux-thunkを使っているので、サーバー送信用のActionで

export const regist = () => (dispatch,getState) => {
  const state = getState();
  ・・・

として全体を取得しました。

ちなみにstoreをimportして直接利用するのは非推奨だそうです。

Store Setup · Redux
ReduxのFAQを読み直す - Qiita

コンポーネントに固有なstate

Reduxを使用するにあたりstateと名のつくものはReduxのStoreで管理することになると勝手に思っていましたが、UIの状態管理などのコンポーネントに固有なstateは通常のstateとして保持してsetStateで状態を切り替える方がシンプルになります。

Organizing State · Redux
ReduxにおけるGlobal stateとLocal stateの共存 - LIVESENSE ENGINEER BLOG

などなど、また新しい発見があったら、更新したいと思います。