Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Android] redux-persist with AsyncStorage is storing data to Sqlite as columns #1265

Closed
sungsong88 opened this issue Dec 10, 2020 · 1 comment

Comments

@sungsong88
Copy link

sungsong88 commented Dec 10, 2020

I have an app that can become quite large in terms of the size of data.
When a user has more than 2000 documents, and he tries to sign in I noticed that redux-persist fails silently and nothing is persisted.

Array with 2000 notes works, but 2001 won't.

I was wondering why this is happening on Android. And bumped into the list of SQLite default config values and saw that SQLITE_MAX_COLUMN is 2000 as default.

All the other default values are huge except SQLITE_MAX_COLUMN and it sorta makes sense. No one wants their tables with 2000 columns, for sure.

But I'm afraid that I think react-persist is giving a brand new column to SQLite for every new element in the array.

The expected behavior is to persist the data as a new row for every new element in the array instead of a new column.(Android SQLite)

Here is how my data storage look like:

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
};

const persistedReducer = persistReducer(persistConfig, combineReducers({
  themeReducer, 
  languageReducer, 
  navigationStateReducer, 
  userReducer, 
  notificationsReducer, 
  mediaReducer, 
  notesReducer, 
  dataManagementReducer, 
  attachmentsReducer, 
  searchReducer,
  appGuideReducer,
  appRatingReducer,
  authsReducer,
  deviceReducer
}));

And for notes reducer, it looks like this, and that array of notes is where it fails when it's greater than 2000:

const initialState = {
  notes: [],
  deleted: []
};

const notesReducer = (state = initialState, action) => {
  if((action.type === "USER_SIGN_IN" || action.type === "USER_SIGN_UP") && action.payload?.notes) {
    return {
      ...state,
      notes: action.payload.notes,
    };
  }
  ...
}

I had also tried to make the entire state as an Object of notes but didn't work either.

const initialState = {};

const notesReducer = (state = initialState, action) => {
  if((action.type === "USER_SIGN_IN" || action.type === "USER_SIGN_UP") && action.payload?.notes) {
    const newNotes = {};
    action.payload.notes.map(note => newNotes[note.id] = note);

    return newNotes;
  }
  ...
}

I don't know how it's done on iOS, but persisting more than 2000 notes working just fine there. Just Android SQLite

@sungsong88
Copy link
Author

Sorry for being a troll.

For nested reducers, I must use the separated persist function.

This is how I solved it:

const persistedReducer = combineReducers({
  themeReducer: persistReducer({
    key: 'theme',
    storage: AsyncStorage,
  }, themeReducer), 
  languageReducer: persistReducer({
    key: 'language',
    storage: AsyncStorage,
  }, languageReducer), 
  navigationStateReducer: persistReducer({
    key: 'navigation_state',
    storage: AsyncStorage,
  }, navigationStateReducer), 
  userReducer: persistReducer({
    key: 'user',
    storage: AsyncStorage,
  }, userReducer), 
  notificationsReducer: persistReducer({
    key: 'notifications',
    storage: AsyncStorage,
  }, notificationsReducer), 
  mediaReducer: persistReducer({
    key: 'media',
    storage: AsyncStorage,
  }, mediaReducer), 
  notesReducer: persistReducer({
    key: 'notes',
    storage: AsyncStorage,
  }, notesReducer), 
  dataManagementReducer: persistReducer({
    key: 'data_management',
    storage: AsyncStorage,
  }, dataManagementReducer), 
  attachmentsReducer: persistReducer({
    key: 'attachments',
    storage: AsyncStorage,
  }, attachmentsReducer), 
  searchReducer: persistReducer({
    key: 'search',
    storage: AsyncStorage,
  }, searchReducer),
  appGuideReducer: persistReducer({
    key: 'app_guide',
    storage: AsyncStorage,
  }, appGuideReducer),
  appRatingReducer: persistReducer({
    key: 'app_rating',
    storage: AsyncStorage,
  }, appRatingReducer),
  authsReducer: persistReducer({
    key: 'auths',
    storage: AsyncStorage,
  }, authsReducer),
  deviceReducer: persistReducer({
    key: 'device',
    storage: AsyncStorage,
  }, deviceReducer)
});

Ashoat added a commit to CommE2E/comm that referenced this issue Apr 30, 2021
Summary:
Android wasn't persisting the Redux store when logged in with the "ashoat" user. I saw the "Row too big to fit into CursorWindow" error, so I Googled around and found this hack in a [GitHub issue](react-native-async-storage/async-storage#537).

The long-term fix is for us to move the database stuff to SQLite. For completeness, here are some other fixes I considered:

1. [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) as storage for `redux-persist` (not confident it will handle huge pages better though)
2. [Separating reducers](rt2zz/redux-persist#1265 (comment)) into distinct `AsyncStorage` keys (not sure what the distribution of size by keys is, and might require a migration)
3. Upgrading `AsyncStorage` and investing [this](https://react-native-async-storage.github.io/async-storage/docs/advanced/db_size) (people on the issue said it didn't help)
4. [Moving](react-native-async-storage/async-storage#528) off of SQLite storage onto Android Room storage (people on the issue said it didn't help)

I think using this hack for now is a good stopgap.

Test Plan: Make sure Android is loading the persisted store correctly when the app starts

Reviewers: palys-swm

Reviewed By: palys-swm

Subscribers: KatPo, Adrian, atul

Differential Revision: https://phabricator.ashoat.com/D1069
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant