React Nativeで外界の状態変化をRedux上で取り扱う

モバイルエンジニアの@hotchemiです。

今回はquipper/redux-rn-misc-enhancerについて軽く紹介させて頂きます。

github.com

外界の状態変化をどう監視するか

モバイルアプリの開発をしていると、アプリ内の状態変化の他に幾つか外界から来るイベントを制御する必要がある場合があります。

具体的には、ネットワークの状態(オンラインなのか、オフラインなのか)、アプリの状態(最前面にいるのか、バックグランドにいるのか)、あるいは他アプリやOSからのイベント通知(AndroidのIntentやBroadcastReceiver等)です。

React NativeのAPIにはNetInfoAppStateなどそれらの状態変化を監視するAPIが生えていますが、単純なEventListener型の通知機構の為必要なComponentで都度subscribe/unsubscribeするというのは少し冗長で面倒です。そもそもグローバルに通知されるイベントなのでこちらも都度監視せずグローバルに取り扱いたいところです。

class AppStateExample extends Component {
  state = {
    appState: AppState.currentState
  }

  componentDidMount() {
    AppState.addEventListener('change', this._handleAppStateChange);
  }

  componentWillUnmount() {
    AppState.removeEventListener('change', this._handleAppStateChange);
  }

  _handleAppStateChange = (nextAppState) => {
    if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {
      console.log('App has come to the foreground!')
    }
    this.setState({appState: nextAppState});
  }

  render() {
    return (
      <Text>Current state is: {this.state.appState}</Text>
    );
  }
}

もしReduxを利用して開発している場合は、Store上のStateとして管理してしまえば良さそうです。そして、それを可能にするのがquipper/redux-rn-misc-enhancerです。

インストール

以下のコマンドから、インストールします。

yarn add redux-rn-misc-enhancer
npm install redux-rn-misc-enhancer

使い方

まず、以下のようにしてenhancerをcreateStoreの第3引数に登録します。enhancerとはcreateStoreを引数に受け取って、ラップしたcreateStoreを返す関数の事です。ライブラリの実装はこんな感じになり、必要に応じてStoreにdispatchするlistenerを関数内で登録しています。

import { compose, createStore } from 'redux';
import {
  applyAppStateListener,
  applyNetInfoListener,
} from 'redux-rn-misc-enhancer';

const enhancer = compose(
  applyAppStateListener(),
  applyNetInfoListener(),
);
const store = createStore(reducers, initialState, enhancer);

次に、Reducerに以下のようにして監視したいstateを登録します。

import { Action, combineReducers, Reducer } from 'redux';
import {
  AppStateInitialState,
  appStateReducer,
  AppStateState,
  NetInfoInitialState,
  netInfoReducer,
  NetInfoState,
} from 'redux-rn-misc-enhancer';

export interface State {
  appState: AppStateState;
  netInfo: NetInfoState;
}

export const initialState: State = {
  appState: AppStateInitialState,
  netInfo: NetInfoInitialState,
};

const appReducer: Reducer<State> = combineReducers({
  appState: appStateReducer,
  netInfo: netInfoReducer,
});

const rootReducer = (state: State, action: Action) => appReducer(state, action);

export default rootReducer;

以上で準備は完了です。ネットワークやアプリの状態を監視したい場合は他のデータと同じくStoreからデータを取得するだけでOKです。

頻繁に更新され得る類のものなのでreselectを併用して無駄なUIの更新を避けるのがbetterでしょう。

import { createSelector } from 'reselect';
import { State } from '../rootReducer';

export const getNetInfoFromStore = (state: State) => state.netInfo;

export const getIsConnected = createSelector(
  getNetInfoFromStore,
  netInfo => (netInfo.isConnected !== undefined ? netInfo.isConnected : true),
);
const mapStateToProps = (state) => {
  return {
    isConnected: getIsConnected(state),
  };
};

おわりに

ちょっとした工夫ですが、外界の状態変化をアプリから取り回しよく扱えるようになりました。

現場からは以上です。