unstatedで作ったグローバルStateは、アプリを終了したら失われますが、これを永続化することでアプリを次に起動したときも前のデータをキープします。
この記事は連載です。親記事はこちら
前提
前回までに、請求書情報をサーバーから取得して表示できるようになったので、今回はサーバーから取得した後と、データを修正した時に、その結果得られるデータ全体を永続化します。
- Macを使います。でもWindows/Linuxでも同じで大丈夫なはず。
- React Nativeの前回までの内容の続きです。
- 前回までに、React Navigation V5 と unstated と Native Base を導入済みです。
永続化には、react-native-community/async-storageを使います。これを使うことで、アプリからスマホ内にデータを保存できます。Expo版詳細はこちら。
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()
を呼びます。
サーバーからデータを取得した時:setState
をsetStateAndSave
に置き換えるだけです。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 }); }); }
データを修正した時: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()
を呼びます。
render() { ... let globalState = new InvoiceContainer({ initialSeeding: true }); globalState.load(); return ( <Provider inject={[globalState]}> <NavigationContainer> <RootStack /> </NavigationContainer> </Provider> ); }
実行すると、最初はInvoiceがない状態になります。
一度データをサーバーからインポート。
一度デバイスを振って(Device -> Shake)Expoメニューを出し、Reloadすると、
起動直後からさきほどのデータが見えます。