LoginSignup
13
8

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-10

この記事は、「【連載】初めてのReact Native + Expo開発環境構築入門」の子記事です。環境などの条件は、親記事をご覧ください。

※ この記事では、すでに非推奨となっているAsyncStorageを利用しています。最新の方法は、こちらの記事におまかせしました。


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

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

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

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

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

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

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自体の書き換えも行われない、つまりユーザー側から見ても保存失敗が結果として見える、というのがポイントです。

        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のままであることに注意してください。

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 });
      });
  }

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8