React Native School is 40% off when you sign up before December 1!

Easily Persist Data with Context and AsyncStorage

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: October 7, 2020

Persisting data between sessions can be simple using AsyncStorage.

Let's look at the following example.

Right now you can increment or decrement the count.

App.js

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((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </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>
);

Run it on Snack:

The problem is that as soon as we restart/refresh the app the count is lost and we restart at zero.

To fix this we'll store the data in AsyncStorage.

The first thing we do is store the count value in AsyncStorage. The easiest way to do this is to use a useEffect hook and add the count as a dependency. That way any time the count value changes we'll store the new value regardless of how it was actually changed.

App.js

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((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);

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

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

// ...

Take note that we convert the value to a string before storing it.

Then we need to "seed" our state with any data that may exist in AsyncStorage.

App.js

// ...

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

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

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

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

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

// ...

You'll notice a few things here.

First, we add another useEffect that will only run on mount (because it has not dependencies). In that effect we grab the current value - if we have a value then we parse it as an integer and update the count with that value.

We also made an update to the useEffect that is run each time the count is changed. This if block ensures that we don't override the value that is stored in AsyncStorage. If that if block is remove the value will always be the initial value.

This is obviously a very basic example but I've used this exact pattern to scale up to storing complex JSON objects.

The final code can be tested on Snack.

App.js

import React, { useContext, createContext, useState, useEffect } from 'react';
import { View, Button, 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((c) => c + 1);
  const decrement = () => setCount((c) => c - 1);

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

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

  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </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>
);
React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.

Join the email list to be notified of all new lessons and classes!