3

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.

8
  • I’ve encountered a similar issue where the size of a single reduced had many mb of data (think around 52!). It seemed crashed on iOS. Not sure if the exact cause, but I suspected that there was an issue generating the string infield value that big. Does that sound like it may be similar to your issue? If so, I may be able to answer with they solution I’ve recently implemented. Commented Feb 1, 2018 at 21:35
  • @RobWalker That may be similar - I don't think I've got quite that much data, but it's certainly possible that a single reducer is around 35MB. Based on feedback from the redux-persist dev I'm trying out the redux-persist-filesystem-storage in place of the default AsyncStorage implementation, which hasn't crashed yet... 🤞 Commented Feb 2, 2018 at 8:07
  • Cool. That’s my package, so let me know if you have any issues with it :) Commented Feb 2, 2018 at 8:09
  • Ha! Nice... I'm thinking about implementing a PouchDB storage engine, since that would give me interesting sync possibilities with CouchDb - it looks like the it wouldn't be that complicated. Commented Feb 2, 2018 at 8:17
  • Cool. I’m also using redux-persist-redux which is working well for me. Commented Feb 2, 2018 at 8:19

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Updated to add missing example code for makeGetAllRecords

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.