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

Expo环境下React Native调用Keychain.setGenericPassword时报null错误

Expo环境下React Native Keychain存储报错解决方案

问题详情

在Expo环境的iPhone设备上开发React Native应用,登录/注册时尝试通过react-native-keychain库将token存入系统Keychain,持续抛出错误:TypeError: Cannot read property 'setGenericPasswordForOptions' of null。已通过console.log确认action.payload为有效字符串token,依赖安装正确,且在Reducer中添加了action.payload非空校验,但问题仍未解决。

原始Reducer代码

const appReducer = (state, action) => {
  switch (action.type) {
    case "SET_USER":
      return { ...state, user: action.payload };
    case "SET_ACCESS":
      console.log(action.payload);
      Keychain.setGenericPassword("ACCESS", action.payload);
      return { ...state, access: action.payload };
    case "SET_REFRESH":
      Keychain.setGenericPassword("REFRESH", action.payload);
      return { ...state, refresh: action.payload };
    default:
      return state;
  }
};

修改后的Reducer代码(仍报错)

const appReducer = (state, action) => {
  switch (action.type) {
    case "SET_USER":
      return { ...state, user: action.payload };
    case "SET_ACCESS":
      if (action.payload) {
        Keychain.setGenericPassword("ACCESS", action.payload);
        return { ...state, access: action.payload };
      }
    case "SET_REFRESH":
      if (action.payload) {
        Keychain.setGenericPassword("REFRESH", action.payload);
        return { ...state, refresh: action.payload };
      }

    default:
      return state;
  }
};

错误原因

  1. Reducer中执行异步操作违规:Reducer必须是纯函数,不能直接在其中调用Keychain.setGenericPassword这类异步IO操作。这种操作不仅会导致状态变化不可预测,还可能在Keychain模块未完成初始化时被调用,引发null引用错误。
  2. Expo环境适配问题:react-native-keychain并非Expo官方适配库,在Expo环境中可能存在初始化延迟或兼容性问题,导致调用方法时模块实例为null。
  3. Switch Case逻辑穿透:修改后的Reducer中,SET_ACCESS case在不满足action.payload非空条件时,未返回原状态或添加break,会直接执行后续SET_REFRESH的逻辑,引发不必要的代码执行。

解决方案

方案1:替换为Expo官方安全存储库(推荐)

Expo官方提供的expo-secure-store完美适配iOS/Android的安全存储需求,无需原生配置,操作更稳定。

  1. 安装依赖:
npx expo install expo-secure-store
  1. 重构代码,将存储操作移出Reducer,放到组件副作用中:
import React, { createContext, useContext, useReducer, useEffect } from "react";
import * as SecureStore from 'expo-secure-store';

const initialState = {
  user: null,
  access: null,
  refresh: null,
};

const appReducer = (state, action) => {
  switch (action.type) {
    case "SET_USER":
      return { ...state, user: action.payload };
    case "SET_ACCESS":
      return { ...state, access: action.payload };
    case "SET_REFRESH":
      return { ...state, refresh: action.payload };
    default:
      return state;
  }
};

const AppContext = createContext();

export const useAppContext = () => useContext(AppContext);

export const AppProvider = ({ children }) => {
  const [state, dispatch] = useReducer(appReducer, initialState);

  // 监听access token变化,存入安全存储
  useEffect(() => {
    if (state.access) {
      SecureStore.setItemAsync('ACCESS', state.access);
    }
  }, [state.access]);

  // 监听refresh token变化,存入安全存储
  useEffect(() => {
    if (state.refresh) {
      SecureStore.setItemAsync('REFRESH', state.refresh);
    }
  }, [state.refresh]);

  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};

方案2:修复react-native-keychain使用问题

若坚持使用react-native-keychain,需完成以下调整:

  1. 修复Reducer逻辑穿透:
const appReducer = (state, action) => {
  switch (action.type) {
    case "SET_USER":
      return { ...state, user: action.payload };
    case "SET_ACCESS":
      if (action.payload) {
        // 禁止在Reducer中调用异步操作,需移到外部
        return { ...state, access: action.payload };
      }
      return state; // 不满足条件时返回原状态,避免穿透
    case "SET_REFRESH":
      if (action.payload) {
        // 禁止在Reducer中调用异步操作,需移到外部
        return { ...state, refresh: action.payload };
      }
      return state; // 不满足条件时返回原状态
    default:
      return state;
  }
};
  1. 配置Expo适配插件:
npx expo install @react-native-keychain/keychain
  1. 修改app.json添加插件:
{
  "expo": {
    "plugins": [
      "@react-native-keychain/keychain"
    ]
  }
}
  1. 重新构建项目:
npx expo prebuild --clean
  1. 将Keychain操作移到组件的useEffect或登录/注册回调中,例如在登录成功后:
// 登录组件示例
const LoginScreen = () => {
  const { dispatch } = useAppContext();

  const handleLogin = async () => {
    // 调用登录接口获取token
    const { accessToken, refreshToken } = await loginApi();
    // 更新状态
    dispatch({ type: "SET_ACCESS", payload: accessToken });
    dispatch({ type: "SET_REFRESH", payload: refreshToken });
    // 存入Keychain
    await Keychain.setGenericPassword("ACCESS", accessToken);
    await Keychain.setGenericPassword("REFRESH", refreshToken);
  };

  return <Button onPress={handleLogin} title="登录" />;
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 09:35:07