React Native + redux と realmの相性が悪かった話


この記事は React Native Advent Calendar 2017 10日目です。

初めに

React Native + Redux で開発中のアプリで、ローカルのdbが欲しくなったのでRealmの導入を検討してみたらいろいろと相性が悪いことに気付いた。
最終的にRealmは使わないことになったが、導入を検討している方の参考になれば。

準備

redux と realm の導入

React Nativeのプロジェクトを作り、以下のコマンドを実行

$ yarn add realm
$ react-native link realm
$ yarn add react-redux
$ react-native link react-redux
$ yarn add redux
$ react-native link redux

これだけでも動きはするのだが、今回確認したいことをするためには追加でライブラリが必要。

$ yarn add redux-immutable-state-invariant
$ react-native link redux-immutable-state-invariant
$ yarn add remote-redux-devtools
$ react-native link remote-redux-devtools

realmのセットアップ

以下のようなModelを定義

item.js
class Item {
  static schema: UserSchema = {
    name: 'Item',
    primaryKey: 'id',
    properties: {
      id: { type: 'string', default: '' }
    },
  }
}

これだけで使えるようになることに関しては、realmは本当に楽でいいと思う。

reduxのセットアップ

以下のようなactionとreducerを作成
プロダクトではflowを導入しているが、ここでは面倒なのでやらない。

action.js
export const SET_ITEMS = 'SET_ITEMS';
export const setItems = (items) => ({
  type: SET_ITEMS,
  items,
});
reducer.js
import { SET_ITEMS } from '../actions/items';

const INITIAL_STATE = {
  items: [],
};

export default (state = INITIAL_STATE, action) => {
  console.log(action);
  switch(action.type) {
    case SET_ITEMS:
    return {
      ...state,
      items: action.items,
    }
    default:
    return state;
  }
};

アプリケーションの作成

アプリ起動時、以下のようにしてreduxのdispatcherを作成する。
redux-immutable-state-invariant が今回のキモ。

import { connect, Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import { composeWithDevTools } from 'remote-redux-devtools';
import reducers from './reducers';

const middlewares = [];
const reduxInvariant = require('redux-immutable-state-invariant');
middlewares.push(reduxInvariant.default());
store = createStore(reducers, composeWithDevTools(applyMiddleware(...middlewares)));
const dispatch = store.dispatch;

このdispatcherを利用するために、以下のような2つの関数を作成する。

const addItemToRealm = () => {
  const id = new Date().toISOString();
  const realm = new Realm({schema: [Item]});
  realm.write(() => {
    realm.create('Item', { id });
  })
};

const loadItemsToStateFromRealm = () => {
  const realm = new Realm({schema: [Item]});
  const items = realm.objects('Item');
  console.log(items);
  const action = setItems(items);
  dispatch(action);
}

最後に、この2つの関数を呼び出せるようなUIを作る。

const styles = StyleSheet.create({
  button1: {
    marginLeft: 100,
    marginTop: 100,
    alignItems: 'center',
    justifyContent: 'center',
    height: 200,
    width: 200,
    backgroundColor: '#F00',
  },
  button2: {
    marginLeft: 100,
    marginTop: 100,
    alignItems: 'center',
    justifyContent: 'center',
    height: 200,
    width: 200,
    backgroundColor: '#0F0',
  },
});

export default class App extends React.Component {
  render() {
    return (
      <View>
        <TouchableOpacity onPress={addItemToRealm}>
          <View style={styles.button1}>
            <Text>Tap here to add item</Text>
          </View>
        </TouchableOpacity>
        <TouchableOpacity onPress={loadItemsToStateFromRealm}>
          <View style={styles.button2}>
            <Text>Tap here to load item</Text>
          </View>
        </TouchableOpacity>
      </View>
    )
  }
}

完成したプロジェクトは↓こちら。動作確認はiOSのシミュレータだけで行っている。
https://github.com/almichest/realm-with-react-native

このアプリを起動すると↓このような画面が表示され、赤いボタンをタップするとrealmにオブジェクトが追加され、緑のボタンをタップするとrealmに保存されたオブジェクトがreduxのstate内に挿入される。

この状態で、赤いボタンを複数回タップしたあとで緑のボタンを2回タップすると↓お馴染みのこんな画面。

store以外がstateを書き換えている、とかなんとか。
だが、上のコードを読んでいただければわかるが、自分で実装した部分では特に怒られるようなことはしていないはず。
恐らくrealm側が内部でstate内のオブジェクトを変更するような処理をしているのだと思う。
redux-immutable-state-invariant を除けば動くが、それはredux本来の使い方から外れることになるので本質的ではない。

対策

というわけで、realmのオブジェクトをそのままstateに入れることは出来ないので、やるなら恐らくこんな風にする必要があって結構ややこしいことになる。

その1 stateに入れられる形に変換する

const loadItemsToStateFromRealm = () => {
  const realm = new Realm({schema: [Item]});
  const items = realm.objects('Item').map(v => {
    const vv = new Item();
    vv.id = v.id;
    return vv;
  });
  const action = setItems(items);
  dispatch(action);
}

毎回変換する必要があるので非効率。
また、realmのlistenerの機能を活用したい場合にはrealmのオブジェクトと変換後のオブジェクトの両方がメモリに乗ってしまって辛い。

その2 各viewでrealmのオブジェクトを直接observeする

各viewでrealmをobserveし、変化があったらsetStateすれば恐らく対応できる。
reduxのstateとrealm, 2つのmodelを見ることになるのできっと管理がとても辛い。

そもそも、よく考えたら realm.delete() とかを呼んだらstateが変わるんだしそもそもstateにrealmのオブジェクトを入れようと思ったのが間違いだった気もする。

最後に

そんなこんなでいろいろとあり、結局realmは使わないことになった。
もしReact Native + redux + realm の組み合わせでアプリを作っている方がいたらどうしているか気になる。