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.
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.