React Native + Expo アプリでunstatedのデータを永続化

この記事は、「[【連載】初めてのReact Native + Expo開発環境構築入門](https://qiita.com/hitotch/items/5142fff638c7805d84d5)」の子記事です。環境などの条件は、親記事をご覧ください。

※ この記事では、すでに非推奨となっているAsyncStorageを利用しています。最新の方法は、[こちらの記事](https://kyokara.com/expo-asyncstorage-mac/)におまかせしました。

---

　unstatedで作ったグローバルStateは、アプリを終了したら失われますが、これを永続化することでアプリを次に起動したときも前のデータをキープします。
　[前回](https://qiita.com/hitotch/items/de06a44b3aa699f0f4cb)までに、請求書情報をサーバーから取得して表示できるようになったので、今回はサーバーから取得した後と、データを修正した時に、その結果得られるデータ全体を永続化します。

　永続化には、React Native本体の[AsyncStorage](https://facebook.github.io/react-native/docs/asyncstorage.html)を使います。実は今後React Native本体から削除される予定で非推奨となっているのですが、[Expoのドキュメント](https://docs.expo.io/versions/latest/react-native/asyncstorage/)ではまだこれを使うように書かれているので、Expoに従います。Expoを使わない場合は、本家が「こっちに乗り換えろ」と誘導しているAsyncStorage（[@react-native-community/async-storage](https://github.com/react-native-community/async-storage)）を使うべきです。

Q. unstatedのコンテナ(グローバルState)全体を永続化しない理由は？
A. 通信中フラグをオンにしている状態などまで永続化したくない。

Q. [unstated-persist](https://github.com/rt2zz/unstated-persist)を使わない理由は？
A. 上記の通り全体を永続化したくないことと、永続化のタイミングを制御したいため。

# unstatedのコンテナに保存と読み込みメソッドを追加

　ローカルストレージにデータを保存する``setAndSaveState()``と、読み込む``load()``を作ります。コードは[本家サンプル](https://facebook.github.io/react-native/docs/asyncstorage.html)または[Expoサンプル](https://docs.expo.io/versions/latest/react-native/asyncstorage/)からコピーして、自前Stateに合致するように少し修正。

```jsx:containers/InvoiceContainer.js
import { AsyncStorage } from 'react-native';
...
export default class InvoiceContainer extends Container {
  constructor(props = {}) {
    super();
    this.state = {
      data: props.initialSeeding ? Seeder.getSeed() : this.getEmptyData(),
      isDataLoading: false
    };
  }
...
  // Save data to the local storage, then setState.
  setStateAndSave = async updateStates => {
    try {
      for (var k in updateStates) {
        await AsyncStorage.setItem(k, JSON.stringify(updateStates[k]));
      }
      this.setState(updateStates);
    } catch (error) {
      // Error saving data
      console.log("storage error");
    }
  };

  // Load data from the local storage
  load = async () => {
    try {
      const value = await AsyncStorage.getItem("data");
      if (value !== null) {
        // Data found
        this.setState({ data: JSON.parse(value) });
      } else {
        this.setState({ data: this.getEmptyData() });
      }
    } catch (error) {
      // Error retrieving data
      console.log("storage error");
    }
  };
...
```

　``AsyncStorage``で保管できるのは文字列だけなので、DataオブジェクトをJSON文字列に変換して保存し、読み出し字はその逆を実施します。ポイントは以下の部分です。

保存：単に``save()``とすると、保存のタイミング制御が難しくなるので、``setState()``と動作を組み合わせた``setStateAndSave()``としています。こうしないと、呼び出し側で``setState()``した直後に``save()``したくなりますが、``setState()``した直後は実際にはStateが更新されていないため、直前の状態を保存してしまう問題が発生します。この問題を意識させないため、setStateと保存を同時に行うメソッドを準備します。中のコードは単純で、指定されたStateについてローカルに保存してからsetStateしています。awaitが入っているので、保存に失敗するとsetStateに到達せず、State自体の書き換えも行われない、つまりユーザー側から見ても保存失敗が結果として見える、というのがポイントです。

```jsx
        for (var k in updateStates) {
          await AsyncStorage.setItem(k, JSON.stringify(updateStates[k]));
        }
        this.setState(updateStates);
```

読み込み：setStateを使っていることに注意。

```jsx
const value = await AsyncStorage.getItem('data');
...
this.setState({data:JSON.parse(value)});
```

# 保存したいタイミングのコーディング

　サーバーからデータを取得した直後や、アプリ内でデータを変更したときに、上で作った``setAndSaveState()``を呼びます。

サーバーからデータを取得した時：``setState``を``setStateAndSave``に置き換えるだけです。``isDataLoading``は永続化したくないので``setState``のままであることに注意してください。

```jsx:containers/InvoiceContainer.js
  getDataFromServer(endpoint) {
    this.setState({ isDataLoading: true });
    console.log(endpoint);
    axios
      .get(endpoint, { params: {} })
      .then(results => {
        console.log("HTTP Request succeeded.");
        console.log(results);
        this.setStateAndSave({ data: results.data });
        this.setState({ isDataLoading: false });
      })
      .catch(() => {
        console.log("HTTP Request failed.");
        this.setState({ isLoading: false });
      });
  }
```

データを修正した時：``setState``を``setStateAndSave``に置き換えるだけです。

```jsx:components/SummaryScreen.js
class SummaryScreenContent extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Summary Screen</Text>
        <Button
          title="Modify Inv#2"
          onPress={() => {
            let data = this.props.globalState.state.data;
            data.invoices[1].date = "2/2/2020";
            this.props.globalState.setStateAndSave({ data: data });
          }}
        />
      </View>
    );
  }
}
```

# 読み込みたいタイミングのコーディング

　アプリを起動したときに、以前のデータを読み込むべきなので、グローバルStateを作った時＝unstatedコンテナのインスタンスを作った時に、``load()``を呼びます。

```jsx:App.js
export default class App extends React.Component {
  ...
  render() {
    ...
    let globalState = new InvoiceContainer({ initialSeeding: true });
    globalState.load();
    return (
      <Provider inject={[globalState]}>
        <AppContainer />
      </Provider>
    );
  }
}