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

Safely migrate from AsyncStorage #52

Closed
Jalson1982 opened this issue Apr 19, 2021 · 10 comments
Closed

Safely migrate from AsyncStorage #52

Jalson1982 opened this issue Apr 19, 2021 · 10 comments
Labels
question Further information is requested

Comments

@Jalson1982
Copy link

Hello. I really want to replace in my app on all the place AsyncStorage and use MMKV. The problem I have is that I use async storage with redux persist and if I want to change to MMKV I will need to log out users as in redux persist I keep info if the user has a profile, authenticated, etc. Any idea maybe how safely do migration if it is possible?

@mrousavy
Copy link
Owner

mrousavy commented Apr 19, 2021

Well yeah, you can just use something like this:

const hasMigrated = MMKV.getBoolean("hasMigratedFromAsyncStorage")
if (!hasMigrated) {
  const keys = await AsyncStorage.getAllKeys();
  const values = await AsyncStorage.multiGet(keys);

  values.forEach(([key, value]) => {
    MMKV.set(key, value)
  });

  MMKV.set("hasMigratedFromAsyncStorage", true)

  await AsyncStorage.clear(); // (optional)
}

See AsyncStorage's getAllKeys, multiGet and clear functions

@mrousavy mrousavy changed the title Help with redux persist Migrate from AsyncStorage Apr 19, 2021
@mrousavy mrousavy changed the title Migrate from AsyncStorage Safely migrate from AsyncStorage Apr 19, 2021
@mrousavy mrousavy added the question Further information is requested label Apr 19, 2021
@tothvoj-gl
Copy link

The migration script is working fine in most cases, but when the apollo-cache-persist item in AsyncStorage is more than 2MB in size, than AsyncStorage.multiGet(keys) will throw an exception. The stacktrace is native C++ pointing to MMKV which is weird and prolonged the investigation of the root cause for us. That's why I wanted to mention here. Below you can find our migration script which wont crash the app if this happens:

const keys = await AsyncStorage.getAllKeys();

 // eslint-disable-next-line no-restricted-syntax
 for (const key of keys) {
   try {
     // eslint-disable-next-line no-await-in-loop
     const value = await AsyncStorage.getItem(key);

     if (value !== null) {
       if (['true', 'false'].includes(value)) {
         MMKV.setBool(key, value === 'true');
       } else {
         MMKV.setString(key, value);
       }

       AsyncStorage.removeItem(key);
     }
   } catch (error) {
     
   }
 }

@mrousavy
Copy link
Owner

Might be an out of memory exception for the MMKV instance? Don't forget that MMKV is an in-memory database

@tothvoj-gl
Copy link

@mrousavy
Copy link
Owner

mrousavy commented Sep 15, 2021

Here's an up-to-date snippet to migrate from AsyncStorage to MMKV using the latest version of both:

storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import {MMKV} from 'react-native-mmkv';

export const storage = new MMKV();

// TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated)
export const hasMigratedFromAsyncStorage = storage.getBoolean(
  'hasMigratedFromAsyncStorage',
);

// TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated)
export async function migrateFromAsyncStorage(): Promise<void> {
  console.log('Migrating from AsyncStorage -> MMKV...');
  const start = global.performance.now();

  const keys = await AsyncStorage.getAllKeys();

  for (const key of keys) {
    try {
      const value = await AsyncStorage.getItem(key);

      if (value != null) {
        if (['true', 'false'].includes(value)) {
          storage.set(key, value === 'true');
        } else {
          storage.set(key, value);
        }

        AsyncStorage.removeItem(key);
      }
    } catch (error) {
      console.error(
        `Failed to migrate key "${key}" from AsyncStorage to MMKV!`,
        error,
      );
      throw error;
    }
  }

  storage.set('hasMigratedFromAsyncStorage', true);

  const end = global.performance.now();
  console.log(`Migrated from AsyncStorage -> MMKV in ${end - start}ms!`);
}
App.tsx
...
import { hasMigratedFromAsyncStorage, migrateFromAsyncStorage } from './storage';
...

export default function App() {
  // TODO: Remove `hasMigratedFromAsyncStorage` after a while (when everyone has migrated)
  const [hasMigrated, setHasMigrated] = useState(hasMigratedFromAsyncStorage);

  ...

  useEffect(() => {
    if (!hasMigratedFromAsyncStorage) {
      InteractionManager.runAfterInteractions(async () => {
        try {
          await migrateFromAsyncStorage()
          setHasMigrated(true)
        } catch (e) {
          // TODO: fall back to AsyncStorage? Wipe storage clean and use MMKV? Crash app?
        }
      });
    }
  }, []);

  if (!hasMigrated) {
    // show loading indicator while app is migrating storage...
    return (
      <View style={{ justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator color="black" />
      </View>
    );
  }

  return (
    <YourAppsCode />
  );
}

@mrousavy
Copy link
Owner

The migration script is now added to the Documentation here, so these comments might be outdated.

@mattijsf
Copy link

May I ask why this migration script is using runAfterInteractions?

@mrousavy
Copy link
Owner

No idea, you can remove it as well

@ghost
Copy link

ghost commented May 22, 2022

@mrousavy instead of reading every item you can use multi get:

-const value = await AsyncStorage.getItem(key);

+const keys = await AsyncStorage.getAllKeys();
+const entries = await AsyncStorage.multiGet(keys);

@DePavlenko
Copy link

DePavlenko commented Jun 7, 2024

@mrousavy
The described migration didn't work for redux-persist in our case because store was initialized before the first app render, so getItem had been called before the whole migration started. So to fix that we just moved the logic inside of getItem:

import AsyncStorage from '@react-native-async-storage/async-storage';
import { Storage } from 'redux-persist';
import { MMKV } from 'react-native-mmkv';

export const MMKVReduxStorage = new MMKV();

// TODO: Remove `hasMigratedToMMKVStorage` after a while (when everyone has migrated)
const hasMigratedToMMKVStorage = MMKVReduxStorage.getBoolean(
  'hasMigratedToMMKVStorage',
);

export const reduxStorage: Storage = {
  setItem: (key, value) => {
    MMKVReduxStorage.set(key, value);
    return Promise.resolve(true);
  },
  getItem: async (key) => {
    let value: string | undefined;
    // TODO: Remove the whole if block after a while (when everyone has migrated)
    if (!hasMigratedToMMKVStorage) {
      try {
        const asyncStorageValue = await AsyncStorage.getItem(key);
        if (asyncStorageValue !== null) {
          if (['true', 'false'].includes(asyncStorageValue)) {
            MMKVReduxStorage.set(key, asyncStorageValue === 'true');
          } else {
            MMKVReduxStorage.set(key, asyncStorageValue);
          }
          value = asyncStorageValue;
          AsyncStorage.removeItem(key);
        }
      } catch (error) {
        AsyncStorage.removeItem(key);
      }
      MMKVReduxStorage.set('hasMigratedToMMKVStorage', true);
    }
    if (!value) {
      value = MMKVReduxStorage.getString(key);
    }
    return Promise.resolve(value);
  },
  removeItem: (key) => {
    MMKVReduxStorage.delete(key);
    return Promise.resolve();
  },
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

5 participants