You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在iOS设备上重置react-redux持久化store以完成测试

问题梳理

背景

我已尝试查阅该问题的相关资料,但仍无法解释iOS设备上react-redux与redux-persist的store配置的实际运行情况。

问题描述

  • QA团队无法测试应用的引导流程体验
  • 引导流程逻辑:3个介绍页(由isNew标志控制)→ 登录 → 引导教程(触发条件为is_first_time_login: true、hasLoggedIn || hasPinCodeLoggedIn: true且hasFinishedOnboardTutorial:false)
  • Android设备测试表现符合预期,iOS设备未正常生效
  • iOS设备已尝试删除应用、重启手机后重新安装通过Firebase Distribution分发的测试包,问题仍未解决

相关代码配置

store/index.js

import { applyMiddleware, compose, createStore } from 'redux';
import { persistReducer, persistStore, createTransform } from 'redux-persist';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import createSensitiveStorage from 'redux-persist-sensitive-storage';
import { NODE_ENV } from '@env';
import isEmpty from 'lodash/isEmpty';
import RootSaga from 'Sagas';
import RootReducer from 'Reducers';
import { UserDetail } from 'Models';
import ApiUtils from 'Services';
import { Configs, reactotron } from 'Configs';

const secureStorage = createSensitiveStorage({
  keychainService: 'xxxx',
  sharedPreferencesName: 'xxx'
});

const PersistStateTransformer = createTransform(
  (inboundState) => {
    return { ...inboundState };
  },
  (outboundState) => {
    if (!isEmpty(outboundState?.token)) {
      ApiUtils.injectToken(outboundState?.token);
    }
    return { ...outboundState, userDetails: new UserDetail(outboundState.userDetails), backgroundTimestamp: 0, hasPinCodeLoggedIn: false };
  },
  {
    whitelist: ['auth'],
  }
);

const persistConfig = {
  key: 'root',
  storage: secureStorage,
  transforms: [PersistStateTransformer],
  whitelist: ['auth', 'ui'],
};

// Use to remove all the existing state including auth/ui
// if (NODE_ENV === 'development') {
//   AsyncStorage.removeItem('persist:root');
// }

export const buildStore = (initialState = {}) => {
  const middlewares = [];
  const enhancers = [];

  // Connect the sagas to the redux store
  const sagaMiddleware = createSagaMiddleware();
  middlewares.push(sagaMiddleware);

  if (NODE_ENV === 'development') {
    middlewares.push(logger);
  }

  // do not remove spread operator
  enhancers.push(applyMiddleware(...middlewares));

  // Redux persist
  const persistedReducer = persistReducer(persistConfig, RootReducer);

  // Reactotron is only needed during dev, in test env it's not necessary
  const store =
    NODE_ENV === 'development'
      ? createStore(persistedReducer, initialState, compose(...enhancers, reactotron.createEnhancer()))
      : createStore(persistedReducer, initialState, compose(...enhancers));
  const persistor = persistStore(store);

  // Kick off the root saga
  sagaMiddleware.run(RootSaga);

  return { store, persistor };
};

App.js

import React, { useEffect } from 'react';
import { Platform, UIManager } from 'react-native';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import SplashScreen from 'react-native-splash-screen';
import Navigation from 'Navigations';
import { buildStore } from 'Stores';
import { Loading } from 'Screens';
import { Toast } from 'Components';
import GlobalUiProvider from 'Contexts';
import ApiUtils from 'Services';

const { store, persistor } = buildStore();
ApiUtils.injectStore(store);

const App = () => {
  if (Platform.OS === 'android') {
    UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
  }

  useEffect(() => {
    SplashScreen.hide();
  }, []);

  return (
    <SafeAreaProvider>
      <Provider store={store}>
        <PersistGate loading={<Loading />} persistor={persistor}>
          <GlobalUiProvider>
            <Navigation />
          </GlobalUiProvider>
          <Toast />
        </PersistGate>
      </Provider>
    </SafeAreaProvider>
  );
};

export default App;
export { store, persistor };

publicNavigation.js(代码片段)

<PublicStack.Navigator screenOptions={Configs.PUBLIC_SCREEN_OPTIONS}>
  {isNew ? (
    <PublicStack.Screen name="Onboard" component={Onboard} />
  ) : (
    <PublicStack.Screen name="Login" component={Login} />
  )}
</PublicStack.Navigator>

问题原因

该问题是iOS Keychain的系统特性导致:
你使用的redux-persist-sensitive-storage在iOS端基于系统Keychain实现,Keychain属于系统级存储,不属于App沙盒范畴,App卸载后对应的Keychain条目不会被系统自动清除,重装App后仍可读取到之前存储的auth、ui持久化状态,导致isNew、is_first_time_login等引导相关标志位保留旧值,无法触发引导流程。
而Android端对应的存储实现是SharedPreferences,属于App沙盒内存储,卸载App时会被同步清除,因此表现正常。
另外你代码中注释掉的AsyncStorage清除逻辑无效,因为你并未使用AsyncStorage作为持久化存储载体,删除AsyncStorage内容不会影响sensitive storage存储的状态。

解决方案

方案1:新增版本校验自动清空

在App启动时比对当前App版本号与持久化存储的版本号,若版本号不一致或无存储的版本号,直接调用persistor.purge()清空所有持久化内容,适配重装、升级场景。
示例逻辑:

// 在buildStore方法中新增逻辑,或在App启动的useEffect中添加
import { Version } from 'react-native';
const persistedVersion = store.getState().ui.appVersion;
if (!persistedVersion || persistedVersion !== Version.appVersion) {
  persistor.purge().then(() => {
    // 清空后刷新状态
    persistor.flush();
  });
}

方案2:新增测试调试入口

针对QA测试场景,在App设置页添加隐藏的调试入口,点击后直接调用persistor.purge()重置所有持久化状态,无需重装App即可测试引导流程。

方案3:修改存储Key隔离版本/环境

修改persistConfig的key字段,添加版本号或环境前缀,不同版本/环境的存储Key不通用,重装或升级后无法读取旧版本存储内容,自动视为首次启动。
示例:

const persistConfig = {
  key: `root_v${Version.appVersion}`, // 追加版本号
  storage: secureStorage,
  transforms: [PersistStateTransformer],
  whitelist: ['auth', 'ui'],
};

内容的提问来源于stack exchange,提问作者mdoxb8

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 18:54:00