Migrating Data in AsyncStorage

Author

Spencer Carli

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

Last Updated: June 26, 2019

You're likely storing some data in AsyncStorage. I can pretty much guarantee that at some point you'll outgrow however you're storing that data. Today I want to talk about how you can go about migrating that data and how you can better structure it in the future so that you can more easily manage data migrations.

Let's start with a simple app that stores favorite foods in AsyncStorage.

App.js

import React from 'react';
import { StyleSheet, Text, View, AsyncStorage } from 'react-native';

export default class App extends React.Component {
  async componentDidMount() {
    await this.initializeData();
    await this.readData();
  }

  initializeData = async () => {
    await AsyncStorage.setItem(
      'favoriteFoods',
      JSON.stringify(['Pizza', 'Burrito'])
    );
  };

  readData = async () => {
    const favoriteFoods = await AsyncStorage.getItem('favoriteFoods');
    console.log('favoriteFoods', favoriteFoods);
  };

  render() {
    return (
      <View style={styles.container}>
        <Text>Open up App.js to start working on your app!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Now let's say that we now want to associate each food item with a relevant id on the server - so we need to store not only the item name but also an associated id. That means an object will be more fitting than a string.

The new data will look like [{ _id: 0, 'Pizza' }, { _id: 1, 'Burrito' }];

We could simply write something like this:

App.js

// ...

export default class App extends React.Component {
  async componentDidMount() {
    await this.initializeData();
    await this.migrateData(); // NEW LINE
    await this.readData();
  }

  // ...

  migrateData = async () => {
    const _favoriteFoods = await AsyncStorage.getItem('favoriteFoods');
    const favoriteFoods = JSON.parse(_favoriteFoods);
    const newFavoriteFoods = favoriteFoods.map((food, index) => {
      return { _id: index, name: food };
    });
    AsyncStorage.setItem('favoriteFoods', JSON.stringify(newFavoriteFoods));
  };

  // ...
}

// ...

However, there are a few problems with this:

  1. What if our migration fails and our data isn't formatted correctly? How do we handle that in the app?
  2. How do we know on future runs of the app if the data has already been migrated?
  3. What happends if/when we need to do another data migration? How do we know what version of data we're coming from?

With those things in mind we want to store a version number alongside the data. I like to keep a version with each piece of data in AsyncStorage rather than a single version.

App.js

// ...

export default class App extends React.Component {
  async componentDidMount() {
    await this.initializeData();
    await this.migrateData(); // NEW LINE
    await this.readData();
  }

  // ...

  migrateData = async () => {
    const _favoriteFoods = await AsyncStorage.getItem('favoriteFoods');
    const favoriteFoods = JSON.parse(_favoriteFoods);
    const newFavoriteFoods = favoriteFoods.map((food, index) => {
      return { _id: index, name: food };
    });
    AsyncStorage.setItem(
      'favoriteFoods',
      JSON.stringify({
        version: 2,
        data: newFavoriteFoods,
      })
    );
  };

  // ...
}

// ...

Now we only want to run the migration once. Once we're at V2 we don't need to keep running this migration.

App.js

// ...

export default class App extends React.Component {
  async componentDidMount() {
    await this.initializeData();
    await this.migrateData(); // NEW LINE
    await this.readData();
  }

  // ...

  migrateData = async () => {
    const _favoriteFoods = await AsyncStorage.getItem('favoriteFoods');
    const favoriteFoods = JSON.parse(_favoriteFoods);
    if (favoriteFoods && !favoriteFoods.version) {
      console.log('running migration of favorite foods to v2');
      const newFavoriteFoods = favoriteFoods.map((food, index) => {
        return { _id: index, name: food };
      });
      AsyncStorage.setItem(
        'favoriteFoods',
        JSON.stringify({
          version: 2,
          data: newFavoriteFoods,
        })
      );
    }
  };

  // ...
}

// ...

And you can continue to do the same thing going forward for future data migrations.

Go ahead and comment out the call to this.initializeData(); and refresh the app a few times. You'll see that the actual data migration is only run once.

You can then go ahead and set up whatever conditionals you may need in your app to handle data that hasn't been migrated yet.

I hope this quick lesson helps you better store and organize persistent data in AsyncStorage!

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.