ReactNativeでRedux-Persistを使ってみる

redux-persistはreduxのstore(state)の情報を自動的にローカルに保存してくれる仕組み。
標準構成では、WebならlocalStorage, ReactNativeならAsyncStorageが利用されるようです。

## 前提

０からRedux環境を作成するのが大変なので[個人的テンプレート](https://qiita.com/zaburo/items/f0a400efbaac933a0780)を利用します。すみません。

```bash
git clone -b 1-4 https://github.com/eizaburo/react-temp-basic　persist
cd persist
git checkout -b master
npm install
```

## 準備

```bash
npm install --save redux-persist
```

## 実装

### createStore.js

コア部分はReducerやstoreの定義部分です（私の場合はcreateStore.js）。
やっていることは、

* persistのコンフィグ(persistConfig)を設定。
* persistのコンフィグとreducer(s)で、persistedReducerを生成。
* createStore(ここではreduxCreateStore)にreducerとしてpersistedReducerを渡す。
* storeとpersistorを戻す。

という感じ。

```diff:createStore.js
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';

+import { persistReducer, persistStore } from 'redux-persist';
+import storage from 'redux-persist/lib/storage';

export default createStore = () => {

    //persistの設定
+    const persistConfig = {
+        key: 'root',
+        storage,
+    }

    //reducerまとめ
+    const rootReducer = combineReducers({
+        userData: userReducer,
+    });

    //congig + reducersでpersistedReducerを作る
+    const persistedReducer = persistReducer(persistConfig, rootReducer);

    //store作成時にreducerとしてpersistedReducerを渡してやる
+    let store = reduxCreateStore(
+        persistedReducer,
+        applyMiddleware(
+            //thunk
+        )
+    );
    //persistorを定義
+    let persistor = persistStore(store);

    //戻す
+    return { store, persistor };

}
```

## App.js

App.jsでは、

* storeとpersistorを生成。
* \<PersistGate></PersistGate>でタグを囲う。

という感じ。

```diff:App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { createStackNavigator } from 'react-navigation';
import { Provider } from 'react-redux';
import createStore from './createStore';

import Home from './screens/Home';
import Detail from './screens/Detail';

+import { PersistGate } from 'redux-persist/integration/react';

const Stack = createStackNavigator(
  {
    Home: { screen: Home },
    Detail: { screen: Detail },
  },
  {
    initialRouteName: 'Home'
  }
);

//storeとpersistorを取得
+const { store, persistor } = createStore();

export default class App extends React.Component {

  render() {
    return (
      <Provider store={store}>
+        <PersistGate loading={null} persistor={persistor}>
          <Stack/>
+        </PersistGate>
      </Provider>
    );
  }
}
```

\<PersistGate>で囲むことでautoRehydrateが実行されるらしい。autoRehydrateとはreducerが呼ばれる度に永続化し、アプリ起動時に自動読み込みをしてくれる機能。

### Home.js

Homeは特に何もしなくて良い。

変更前は、起動時、initialStateからhogeが表示されており、updateName()を行うことでfoo@Homeに表示が更新される。ここで、アプリを更新すると(command + R)表示は再びhogeに変わるという動きだったが、Redux-Persistの機能により、更新後もfoo@Homeが維持されている。

なお、ボタンを追加し、保存をクリアするpersistor.purge()を実行できるようにしている。

実行後アプリを更新すると再びhogeが表示されるようになる。

```diff:screens/Home.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';
import { updateName } from '../actions/userAction';

+import createStore from '../createStore';

+const { persistor } = createStore();

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
                <Text>{this.props.state.userData.user.name}</Text>
                <Button
                    title='updateName'
                    onPress={() => this.props.updateName('foo@Home')}
                />
+                <Button
+                    title='persist reset'
+                    onPress={() => persistor.purge()}
+                />
            </View>
        );
    }
}

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

const mapDispatchToProps = dispatch => (
    {
        updateName: (name) => dispatch(updateName(name)),
    }
);

export default connect(mapStateToProps, mapDispatchToProps)(Home);
```

## 応用（保存するstateの制限）

ここまでの記述では全てのstateがpersistにより保存されます。
persistConfigにてwhitelistを設定することで保存するstateを選択することが可能です。

## createStore.js

複数のreducerが存在する場合、whitelistに登録すれば、その値だけがpersistの対象になります。

```diff:createStore.js
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';
import configReducer from './reducers/configReducer';

import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';

export default createStore = () => {

    //persistの設定
    const persistConfig = {
        key: 'root',
        storage,
+       whitelist: ['userData'],
    }

    //reducerまとめ
    const rootReducer = combineReducers({
+       userData: userReducer,
+       configData: configReducer,
    });

    //congig + reducersでpersistedReducerを作る
    const persistedReducer = persistReducer(persistConfig, rootReducer);

    //store作成時にreducerとしてpersistedReducerを渡してやる
    let store = reduxCreateStore(
        persistedReducer,
        applyMiddleware(
            //thunk
        )
    );
    //persistorを定義
    let persistor = persistStore(store);

    //戻す
    return { store, persistor };

}
```

### configAction.js

いちおうActionとかも書いておきます。

```js:configAction.js
export const updateConfig = flag => (
    {
        type: 'UPDATE_FLAG',
        flag: flag
    }
);
```

### configReducer.js

Reducerも。

```js:configReducer.js
const initialState = {
    config: {
        flag: 'ON',
    }
}

const configReducer = (state = initialState, action) => {
    switch(action.type){
        case 'UPDATE_FLAG':
            return Object.assign({}, state, {
                config: {
                    flag: action.flag
                }
            });
        default:
            return state;
    }
}

export default configReducer;
```

### Home.js

Homeにそれそれを表示し、変更機能を付けます。
で、アプリ自体を更新すると、updateNameの結果は残りますが、updateConfigの結果は初期値に戻ります。

```diff:screens/Home.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import { connect } from 'react-redux';
import { updateName } from '../actions/userAction';
+import { updateConfig } from '../actions/configAction';

import createStore from '../createStore';

const { persistor } = createStore();

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text>Home</Text>
                <Button
                    title='Link to Detail'
                    onPress={() => this.props.navigation.navigate('Detail')}
                />
                <Text>{this.props.state.userData.user.name}</Text>
+                <Text>{this.props.state.configData.config.flag}</Text>
                <Button
                    title='updateName'
                    onPress={() => this.props.updateName('foo@Home')}
                />
+                <Button
+                    title='updateConfig'
+                    onPress={() => this.props.updateConfig('OFF')}
+                />
                <Button
                    title='persist reset'
                    onPress={() => persistor.purge()}
                />
            </View>
        );
    }
}

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

const mapDispatchToProps = dispatch => (
    {
        updateName: (name) => dispatch(updateName(name)),
+        updateConfig: (flag) => dispatch(updateConfig(flag)),
    }
);

export default connect(mapStateToProps, mapDispatchToProps)(Home);
```

こんな感じ。

![スクリーンショット 2018-11-20 11.57.16.png](https://qiita-image-store.s3.amazonaws.com/0/55188/ee5a1226-84b9-d7fb-3071-ae29cf698210.png)

## 内容を暗号化する

[redux-persist-transform-encrypt](https://github.com/maxdeviant/redux-persist-transform-encrypt)を使うと保存内容を暗号化できるみたいです。

が、私の環境（ReactNative環境）だとasyncがうまく機能しませんでした。以下、同期での処理。

```bash
npm install --save redux-persist-transform-encrypt
```

### createStore.js

暗号・復号化用の処理を作成し、transformsに追加します。

```diff:createStore.js
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux';
import userReducer from './reducers/userReducer';
import configReducer from './reducers/configReducer';

import { persistReducer, persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
+import createEncryptor from 'redux-persist-transform-encrypt';

export default createStore = () => {

+    const encryptor = createEncryptor({
+        secretKey: 'hoge',
+    });

    //persistの設定
    const persistConfig = {
        key: 'root',
        storage,
        whitelist: ['userData'],
+        transforms: [encryptor]
    }

    //reducerまとめ
    const rootReducer = combineReducers({
        userData: userReducer,
        configData: configReducer,
    });

    //congig + reducersでpersistedReducerを作る
    const persistedReducer = persistReducer(persistConfig, rootReducer);

    //store作成時にreducerとしてpersistedReducerを渡してやる
    let store = reduxCreateStore(
        persistedReducer,
        applyMiddleware(
            //thunk//
        )
    );
    //persistorを定義
    let persistor = persistStore(store);

    //戻す
    return { store, persistor };

}
```

### 確認

いちおうAsyncStorageの中身を確認してみます。App.jsのDidMount()等に下記関数を追加して確認します。

```js
 getData = async () => {
    const keys = await AsyncStorage.getAllKeys();
    const value= await AsyncStorage.getItem('persist:root');
    console.log(keys);
    console.log(value);
  }
```

persistConfigで指定したkeyはpersist:keyという形で参照できるようです。

```bash
Array [
[08:42:54]   "persist:root",
[08:42:54] ]
[08:42:54] {"userData":"\"U2FsdGVkX19YzsdlkCQnh32/5aITaqAeXJVoW8g2tryLj+KWal3zokyr0D9G+ZVL\"","_persist":"\"U2FsdGVkX1/H579iUfhOkM5Jegh0b8VRsOtXugi/1FzPuNtEtUpSv7ZQeIf/VBZJI/K+WohErxvMAybKfLIxCA==\""}
```

暗号・複合処理はtransformを利用して独自に実装することもできるみたいです。
どんなアルゴリズムか不安な場合は独自実装したほうがいいかもしれません。

* https://stackoverflow.com/questions/45137911/react-native-persist-and-encrypt-user-token-redux-persist-transform-encrypt-er?rq=1