Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事は 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内に挿入される。

Simulator Screen Shot - iPhone 7 - 2017-12-09 at 11.48.45.png

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

Simulator Screen Shot - iPhone 7 - 2017-12-09 at 11.50.43.png

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 の組み合わせでアプリを作っている方がいたらどうしているか気になる。

yamazaki_sensei
車椅子ユーザーなiOSエンジニアです。 が、最近はts + prettierの楽さにハマりつつある。 タンパク質のことがちょっとだけわかります。
https://yamazaki-sensei.hatenablog.com/
toreta
飲食店向け予約/顧客台帳サービス「トレタ」、超直前予約アプリ「トレタnow」を開発・提供するスタートアップ企業です。
https://corp.toreta.in
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした