How and where to save the whole redux store using AsyncStorage

Luis Rizo picture Luis Rizo · Feb 27, 2017 · Viewed 13.3k times · Source

I was wondering if I have to do something like this to save the whole store of my app using AsyncStorage in my react-native app and check if it exists when the app is starting.

var store;

_loadInitialState = async () => {
  try {
    var value = await AsyncStorage.getItem(STORAGE_KEY);
    if (value !== null){
      store = value;
      console.log('Recovered selection from disk: ' + value);
    } else {
      store = configureStore({});
      console.log('Initialized with no selection on disk.');
    }
  } catch (error) {
    console.log('AsyncStorage error: ' + error.message);
  }
};



export default class App extends Component {

  componentWillMount(){
    this._loadInitialState().done()
  }

  render(){
    return(
      <Provider store={store}>
        <AppContainer/>
      </Provider>
    )
  }
}

This is not working but I hope you get the idea.

Answer

dhorelik picture dhorelik · Feb 27, 2017

Yes, this is one of the ways your app can retrieve its state from AsyncStorage. However, the way you want to handle data depends on the app and on the data itself.

Personally I tend not to save the whole app store. Some sections of your app might need to be re-fetched when navigate between scenes, it doesn't make sense to keep that in AsyncStorage. Same goes about the current scene and similar data, that is to be kept in redux store, but will probably be reset on app restart.

Regarding saving the data to AS, the way I'd recommend is redux middleware. You intercept the actions you need, and do AsyncStorage.setItem(). I guess it's ok to do that elsewhere, like in the components code. It's not that clean, but it's up to your taste, really. Note regarding the keys - I just store different datasets in different AsyncStorage keys. This way you handle less data on every store update (not the whole state tree, but it's section only). The middleware code will look similar to the following:

export default store => next => (action) => {
  let result = next(action)

  switch (action.type) {
    case THE_ACTION_THAT_REQUIRES_AN_ASYNCSTORAGE_UPDATE:
      AsyncStorage.setItem('@your_key', action.data_that_is_to_be_stored)
      break

    case ANOTHER_ACTION:
      // similar here
      break

    default:
      break
  }

  return result
}

Note that you don't need to do await here since in most cases it's ok to write data asynchronously (unless you are going to read and use it right away, which shouldn't be the case if you are using redux store)

Regarding app start - you can use AsyncStorage.multiGet to get all keys right away and put them in store. Following the RN docs:

AsyncStorage.getAllKeys((err, keys) => {
  AsyncStorage.multiGet(keys, (err, stores) => {
   stores.map((result, i, store) => {
     let key = store[i][0]
     let value = store[i][1]

     // add your data to redux store
    })
  })
})

Once you read the AsyncStorage and get the data, it makes sense to combine it into a preloaded state object that will be similar to your app store and set the Redux store via a single action. You can call it APP_STATE_INIT, pass the newly combined object to it and catch the action in your reducers which need to use the data. This way you'll just have a single action to rehydrate your state on app start.

You'll probably need some loading state that will affect the loader being displayed until the AS values get fetched and the data gets populated into redux store. But overall you are right, you can put the AsyncStorage init and wait time to componentWillMount

...
async componentWillMount() {
  await _loadInitialState()

  this.setState({
    isLoading: false
  })
}
...

If you think that for your app it's ok to save everything and boot up based on that, feel free to use one of the packages that give a simple way to rehydrate app store from storage. I'd just note potential effect on performance and the fact that this way you lose control over what's going on.