開発環境を作る Mac

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


 unstatedで作ったグローバルStateは、アプリを終了したら失われますが、これを永続化することでアプリを次に起動したときも前のデータをキープします。

この記事は連載です。親記事はこちら

前提

 前回までに、請求書情報をサーバーから取得して表示できるようになったので、今回はサーバーから取得した後と、データを修正した時に、その結果得られるデータ全体を永続化します。

  • Macを使います。でもWindows/Linuxでも同じで大丈夫なはず。
  • React Nativeの前回までの内容の続きです。
  • 前回までに、React Navigation V5 と unstated と Native Base を導入済みです。

 永続化には、react-native-community/async-storageを使います。これを使うことで、アプリからスマホ内にデータを保存できます。Expo版詳細はこちら

なぜ?

Q. React Native 本体に付属するasync-storageを使わない理由は?
A. 非推奨になりました。

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

Q. unstated-persistを使わない理由は?
A. 上記の通り全体を永続化したくないことと、永続化のタイミングを制御したいため。

async-storageのインストール

 react-native-community/async-storageをインストール。

$ expo install @react-native-community/async-storage

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

 ローカルストレージにデータを保存するsetAndSaveState()と、読み込むload()を作ります。コードは本家サンプルからコピーして、自前Stateに合致するように少し修正。

import AsyncStorage from '@react-native-community/async-storage';
...
export default class InvoiceContainer extends Container {
...
  // 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自体の書き換えも行われない、つまりユーザー側から見ても保存失敗が結果として見える、というのがポイントです。

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

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

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

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

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

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

  getDataFromServer() {
    this.setState({ isDataLoading: true });
    axios
      .get(INVOICE_API_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({ isDataLoading: false });
      });
  }

データを修正した時:setStatesetStateAndSaveに置き換えるだけです。

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()を呼びます。

  render() {
...    
    let globalState = new InvoiceContainer({ initialSeeding: true });
    globalState.load();
    return (
      <Provider inject={[globalState]}>
        <NavigationContainer>
          <RootStack />
        </NavigationContainer>
      </Provider>
    );
  }

 実行すると、最初はInvoiceがない状態になります。

一度データをサーバーからインポート。

一度デバイスを振って(Device -> Shake)Expoメニューを出し、Reloadすると、

起動直後からさきほどのデータが見えます。

- 開発環境を作る, Mac

© 2024 今日からプログラマー