How To Easily Persist Your Data With Context and AsyncStorage

How To Easily Persist Your Data With Context and AsyncStorage

It's fairly common to persist user data between sessions in most mobile apps. To get this done in React Native can be as simple as using AsyncStorage. This will allow us to keep changes, or updates to any components even if the app is restarted, refreshed or closed.

Take this counter for example, if you increment or decrement the count and restart the app, the count will reset back to zero.

import React, { useContext, createContext, useState } from 'react';
import { View, Button, Text } from 'react-native';

const CounterContext = createContext(0);

const useCounter = () => useContext(CounterContext);

const CounterContextProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount((value) => value + 1);
  const decrement = () => setCount((value) => value - 1);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    &lt;/CounterContext.Provider>
  );
};

const App = () => {
  const { count, increment, decrement } = useCounter();

  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>{count}</Text>
      <Button title="Increment" onPress={() => increment()} />
      <Button title="Decrement" onPress={() => decrement()} />
    </View>
  );
};

export default () => (
  <CounterContextProvider>
    <App />
  </CounterContextProvider>
);

To fix that let's implement AsyncStorage.

First store the count value in AsyncStorage with the useEffect hook and add the count as a dependency. By doing so every time the count value is updated it'll be stored.

import React, { useContext, createContext, useState, useEffect } from 'react';
import { View, Button, Text } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';

// ...

const CounterContextProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => setCount((value) => value + 1);
  const decrement = () => setCount((value) => value - 1);

  useEffect(() => {
    AsyncStorage.setItem('COUNTER_APP::COUNT_VALUE', `${count}`);
  }, [count]);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    &lt;/CounterContext.Provider>
  );
};

// ...

Don't forget to convert the value to a string before storing it.

Now retrieve the state with the existing data from AsyncStorage.

// ...

const INITIAL_COUNT = 0;
const CounterContextProvider = ({ children }) => {
  const [count, setCount] = useState(INITIAL_COUNT);

  const increment = () => setCount((value) => value + 1);
  const decrement = () => setCount((value) => value - 1);

  useEffect(() => {
    AsyncStorage.getItem('COUNTER_APP::COUNT_VALUE').then((value) => {
      if (value) {
        setCount(parseInt(value));
      }
    });
  }, []);

  useEffect(() => {
    if (count !== INITIAL_COUNT) {
      AsyncStorage.setItem('COUNTER_APP::COUNT_VALUE', `${count}`);
    }
  }, [count]);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    &lt;/CounterContext.Provider>
  );
};

// ...

Notice the new useEffect hook to retrieve data from AsyncStorage will only run on mount because it has no dependencies, then It grabs the existing value and parses it as an integer before updating the count with that value.

Also the existing useEffect to store data in AsyncStorage now checks to make sure the stored value won't be overridden by the initial value in the state.

This basic pattern can easily be scaled up to store more complex JSON objects.

Here is the final code for your reference.

import React, { useContext, createContext, useState, useEffect } from 'react';
import { View, Button, StyleSheet, Text } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';

const CounterContext = createContext(0);

const useCounter = () => useContext(CounterContext);

const CounterContextProvider = ({ children }) => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount((value) => value + 1);
  };
  const decrement = () => setCount((value) => value - 1);

  useEffect(() => {
    if (count !== 0) {
      AsyncStorage.setItem('COUNTER_APP::COUNT_VALUE', `${count}`);
    }
  }, [count]);

  useEffect(() => {
    AsyncStorage.getItem('COUNTER_APP::COUNT_VALUE').then((value) => {
      if (value) {
        setCount(parseInt(value));
      }
    });
  }, []);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    &lt;/CounterContext.Provider>
  );
};

const Separator = () => <View style={styles.separator} />;

const App = () => {
  const { count, increment, decrement } = useCounter();

  return (
    <View style={styles.container}>
      <Text style={styles.counterText}>{count}</Text>
      <Separator />
      <View style={styles.group}>
        <Button
              title="Increment"
              color="#ff5f00"
              onPress={() => increment()}
          style={styles.button}
        />
        <Button
              title="Decrement"
              color="#ff5f00"
              onPress={() => decrement()}
          style={styles.button}
        />
      </View>
    </View>
  );
};

export default () => (
  <CounterContextProvider>
    <App />
  </CounterContextProvider>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 16,
  },
  counterText: {
    textAlign: 'center',
    fontSize: 20,
  },
  separator: {
    marginVertical: 8,
    borderBottomColor: '#737373',
    borderBottomWidth: StyleSheet.hairlineWidth,
  },
  group: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingHorizontal: 18,
    paddingVertical: 8,
  },
});