この記事は、「【連載】初めての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に合致するように少し修正。
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()
を呼びます。
サーバーからデータを取得した時:setState
をsetStateAndSave
に置き換えるだけです。isDataLoading
は永続化したくないのでsetState
のままであることに注意してください。
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
に置き換えるだけです。
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()
を呼びます。
export default class App extends React.Component {
...
render() {
...
let globalState = new InvoiceContainer({ initialSeeding: true });
globalState.load();
return (
<Provider inject={[globalState]}>
<AppContainer />
</Provider>
);
}
}