Search code examples
iosreact-nativereduxasyncstorageredux-persist

Crashes when using Redux-Persist with React Native on iOS device


I've been trying to use redux-persist in a React Native app that I'm building, but whenever I do, I get random crashes when running on an iOS device. This problem does not occur when running on the simulator. It happens on versions 4 and 5 of redux-persist.

According to sentry.io, the error is:

EXC_BREAKPOINT main fatalException 6, Code 2390738480, Subcode 8

I've tested this on all released versions of iOS 11. Sometimes it seems that the error has disappeared for a few days of normal usage, but then it will return, often repeatedly. This behaviour suggests that the crash only occurs when the store reaches a certain state, or if it becomes too large. As far as I can tell, though, this is not the case - apparently Async Storage does not have a size limit on iOS (unlike Android).

The sentry.io stack trace doesn't seem to provide any clues, but here it is anyway:

  JavaScriptCore      0x18e7fc630  bmalloc::Heap::allocateLarge(std::__1::lock_guard&, unsigned long, unsigned long)
  JavaScriptCore      0x18e7f9ea4  bmalloc::Allocator::allocateLarge(unsigned long)
  JavaScriptCore      0x18deb8a04  WTF::fastMalloc(unsigned long)
  JavaScriptCore      0x18dec9d50  WTF::StringImpl::createUninitialized(unsigned int, unsigned short*&)
  JavaScriptCore      0x18dec9bf8  WTF::StringBuilder::allocateBufferUpConvert(unsigned char const*, unsigned int)
  JavaScriptCore      0x18e7e76f0  WTF::StringBuilder::appendQuotedJSONString(WTF::String const&)
  JavaScriptCore      0x18e55a2cc  JSC::Stringifier::appendStringifiedValue(WTF::StringBuilder&, JSC::JSValue, JSC::Stringifier::Holder const&, JSC::PropertyNameForFunctionCall const&)
  JavaScriptCore      0x18e55b354  JSC::Stringifier::Holder::appendNextProperty(JSC::Stringifier&, WTF::StringBuilder&)
  JavaScriptCore      0x18e55a5d4  JSC::Stringifier::appendStringifiedValue(WTF::StringBuilder&, JSC::JSValue, JSC::Stringifier::Holder const&, JSC::PropertyNameForFunctionCall const&)
  JavaScriptCore      0x18e5594e0  JSC::Stringifier::stringify(JSC::Handle)
  JavaScriptCore      0x18e55d804  JSC::JSONProtoFuncStringify(JSC::ExecState*)
  JavaScriptCore      0x18e5f34c8  llint_entry
  JavaScriptCore      0x18e5f2a94  llint_entry
  JavaScriptCore      0x18e5f2a94  llint_entry
  JavaScriptCore      0x18e5f2a94  llint_entry
  JavaScriptCore      0x18e5f2a30  llint_entry
  JavaScriptCore      0x18e5f2ee0  llint_entry
  JavaScriptCore      0x18e5f2a30  llint_entry
  JavaScriptCore      0x18e5f2a94  llint_entry
  JavaScriptCore      0x18e5f2a30  llint_entry
  JavaScriptCore      0x18e5ebf50  llintPCRangeStart
  JavaScriptCore      0x18e4d1b94  JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*)
  JavaScriptCore      0x18def71b8  JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&)
  JavaScriptCore      0x18e506c3c  JSC::boundThisNoArgsFunctionCall(JSC::ExecState*)
  JavaScriptCore      0x18e5ec098  vmEntryToNative
  JavaScriptCore      0x18def7200  JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&)
  JavaScriptCore      0x18e14a1fc  JSC::profiledCall(JSC::ExecState*, JSC::ProfilingReason, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&)
  JavaScriptCore      0x18def6f68  JSObjectCallAsFunction
  rizzle              0x1013eca4c  facebook::react::Object::callAsFunction(OpaqueJSValue*, int, OpaqueJSValue const* const*) const
  rizzle              0x10140c76c  facebook::react::JSCExecutor::callFunction(std::__1::basic_string, std::__1::allocator > const&, std::__1::basic_string, std::__1::allocator > const&, folly::dynamic const&)
  at setJSResponder(node_modules/react-native-sentry/lib/NativeClient.js:155:29)
  at onChange(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3241:23)
  at setResponderAndExtractTransfer(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3354:140)
  at extractEvents(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3463:85)
  at extractEvents(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:2971:54)
  at fn(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3201:47)
  at batchedUpdates(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:2448:20)
  at batchedUpdates(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:198:16)
  at _receiveRootNodeIDEvent(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3221:32)
  at apply(node_modules/react-native/Libraries/Renderer/ReactNativeFiber-prod.js:3234:37)
  at fn(node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:299:42)
  at __guard(node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:262:7)
  at value(node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js:110:10)
  rizzle              0x10140bb00  std::__1::function::operator()(OpaqueJSContext*) const
  rizzle              0x10139a110  facebook::react::tryAndReturnError(std::__1::function const&)
  rizzle              0x1013923a8  facebook::react::RCTMessageThread::tryFunc(std::__1::function const&)
  CoreFoundation      0x18756016c  __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
  CoreFoundation      0x18755fa3c  __CFRunLoopDoBlocks
  CoreFoundation      0x18755dca4  __CFRunLoopRun
  CoreFoundation      0x18747e2d8  CFRunLoopRunSpecific
  rizzle              0x101374c48  +[RCTCxxBridge runRunLoop]
  Foundation          0x187fa7860  __NSThread__start__
  libsystem_pthread   0x1871e432c  _pthread_body
  libsystem_pthread   0x1871e41f8  _pthread_start

I've been trying to get to the bottom of this for over 3 months now, and have been trying out alternative libraries, but I would really like to be able to use redux-persist.


Solution

  • I've encountered the exact same error and stack trace with redux-persist, which seemed to be caused when the size of data in one of my reducers was too large for some aspect of the storage engine to handle.

    In my case, the json that was trying to be persisted was 520000000 characters in size!

    In the end, the best solution I found was to split that persisted reducer into smaller reducers, which I then combined in my app, so that I could handle it as one from my application code.

    I also experimented with different storage engines to find the one that handled that amount of data the best, finally settling on a redux-persist-realm, though we did patch it to fix some errors we were getting.

    To split the reducer I used the first character of each items uuid, so that I would get 16 reasonable even sized sub-sets.

    First I created a reducer for each of the potential first characters of the uuid

    const store = createStore(
        {
          ... other reducers
          recordsDataA: createReducer('A'),
          recordsDataB: createReducer('B'),
          recordsDataC: createReducer('C'),
          recordsDataD: createReducer('D'),
          recordsDataE: createReducer('E'),
          ...
        },
        applyMiddleware(...middlewares),
        enhancer
    )
    

    export const createReducer = key => combineReducers({
      records: recordsReducer(key),
    })
    

    const recordsReducer = batch => (state = [], action) => {
      switch (action.type) {
    
        case REQUEST_RECORDS_SUCCESS:
          return action.records.filter(record => record.id[0].toUpperCase() === batch)
    
        ... other actions
    
        default:
          return state
      }
    }
    

    Then in my selector, using reselect, I combined the individual reducers back into a single array, so that it can easily be used from other selectors. i.e -

    export const makeGetAllRecords = () =>
      createSelector(
        [
          state => state.recordsDataA.records,
          state => state.recordsDataB.records,
          state => state.recordsDataC.records,
          state => state.recordsDataD.records,
          state => state.recordsDataE.records,
          state => state.recordsDataF.records,
          ...
        ],
        (...allBatches) => [].concat(...allBatches)
      )
    

    export const getRecordById = (state: Array<Record>, recordId: string) =>
      makeGetAllRecords()(state).find(record => record.id === recordId) || null
    

    I'm sure there is plenty that can be done to neaten and optimise that code, but its working well for as in production as is.