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

如何修复React Native登录时Redux dispatch间的状态突变错误?

问题描述

点击登录按钮时出现以下错误,无法定位原因及解决方法:

错误信息:Invariant failed: 在两次dispatch之间检测到状态突变,路径为 'auth.user._2'。这可能导致错误行为。


相关代码

LoginScreen

import React, {useState, useLayoutEffect} from 'react';
import {Alert, Image, Text, TouchableOpacity, View,  StyleSheet} from 'react-native';
import {TextInput} from 'react-native-paper';

import Container from '../components/Container';
import Content from '../components/Content';
import { useNavigation } from "@react-navigation/native";
import { login } from "./../actions/auth";
import { useDispatch } from "react-redux";

const LoginScreen  = ({navigation}) => {

  const navigationTo = useNavigation();
  const [passwordVisible, setPasswordVisible] = useState(true);

  const [email, setEmail] = useState();
  const [password, setPassword] = useState();
  const [loader, setLoader] = useState(false);
  const [loginText, setLoginText] = useState('Sign In');
  const dispatch = useDispatch();

  const onConnect = () => {
    let userLogin = {
      email: email,
      password: password,
    };
    if(email && password) {
      setLoader(true);
      setLoginText('Sign In...');
      dispatch(login(userLogin))
        .then((response) => {
          if (response.status == "success") {
            navigation.replace("Home");
          }
        })
        .catch((error) => {
          setLoader(false);
          alert('Your credentials are invalid!');
          setLoginText('Sign In');
        });
    }
  };

  useLayoutEffect(() => {
    navigation.setOptions({
      headerShown: false,
    });
  }, []);

  return (
    <Container insets={{top: true, bottom: true}}>
      <Content>
        <View style={{flex: 1}}>
          <View style={styles.topContainer}>
            <View style={{flexDirection: 'row', alignItems: 'center'}}></View>
            <Image
              style={styles.logo}
              source={require('../assets/xorixBlack.png')}
            />
          </View>

          <View style={styles.keyboardView}>
            <TextInput
              theme={{colors: {text: '#000'}}}
              placeholder="Email address"
              placeholderTextColor="black"
              selectionColor="black"
              style={styles.textInput}
              textColor={'black'}
              value={email}
              onChangeText={(text) => setEmail(text)}
            />

            <TextInput
              theme={{colors: {text: 'black'}}}
              placeholder="Password"
              placeholderTextColor="black"
              value={password}
              onChangeText={(text) => setPassword(text)}
              style={styles.textInput}
              selectionColor="black"
              secureTextEntry={passwordVisible}
              textColor={'black'}
              right={
                <TextInput.Icon
                  color={'black'}
                  name={passwordVisible ? 'eye-off' : 'eye'}
                  onPress={() => setPasswordVisible(!passwordVisible)}
                />
              }
            />
            <TouchableOpacity
              onPress={() => onConnect()}
              disabled={loader}
              style={styles.loginBtn}>
              <Text style={styles.loginText}>{loginText}</Text>
            </TouchableOpacity>

            <View style={{alignItems: 'center', padding: 10}}>
              <View style={styles.text}>
                <Text style={{fontSize: 12, color: 'black'}}>
                  Forgot your password?{' '}
                </Text>
                <Text style={styles.help}> Click here to reset</Text>
              </View>

              <View style={styles.seperatorStyle}>
                <View style={styles.seperator} />
                <Text style={{color: 'black'}}> OR </Text>
                <View style={styles.seperator} />
              </View>

              <View style={styles.facebook}>
                <TouchableOpacity
                  onPress={() => navigationTo.navigate("Home")}>
                  <Text style={styles.faceText}>Sign Up</Text>
                </TouchableOpacity>
              </View>
            </View>
          </View>

  
        </View>
      </Content>
    </Container>
  );
};
export default LoginScreen;

actions/auth.js

import { LOGIN_SUCCESS, LOGOUT } from "./type";
import AuthService from "../services/authService";
export const login = (user) => (dispatch) => {
  return AuthService.logIn(user).then(
    (response) => {
        console.log(response)
      if (response.status === "success") {
        dispatch({
          type: LOGIN_SUCCESS,
          payload: { user: response.user },
        });
Promise.resolve();
        return response;
      }
    },
    (error) => {
      const message = error.toString();
Promise.reject();
      return message;
    }
  );
};

export const logout = () => (dispatch) => {
  return AuthService.logOut().then((response) => {
    if (response.status === "success") {
      dispatch({
        type: LOGOUT,
      });
      Promise.resolve();
      return response;
    }
  });
};

services/authService.js

import AsyncStorage from "@react-native-async-storage/async-storage";
const logIn = async (user) => {
    console.log("user info", user);
    const { email, password } = user;
    return fetch('https://myapi.com/api/orders/login.json', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json;charset=UTF-8', "Access-Control-Allow-Origin": "*", "Accept": "application/json" },
      body: JSON.stringify({
        "email": email,
        "password": password
      })
    })
    .then((response) => response.json())
    .then((responseJson) => {
      if(responseJson.json.error === false) {
        console.log('successfulll');
        AsyncStorage.setItem("user", JSON.stringify(responseJson.json.user));
        return {
          status: "success",
          message: "You are redirecting to home page",
          user: email,
        };
      } else {
        return {
          status: "error",
          message: "Wrong credentials"
        };
      }
    })
    .catch((error) => {
      return {
        status: "error",
        message: "API call failed"
      };
    });
  };
const logOut = async () => {
  AsyncStorage.clear();
  return {
    status: "success",
    message: "You are logged out",
  };
};
export default {
  logIn,
  logOut,
};

authSlice.js

import { LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT } from "../actions/type";
import AsyncStorage from "@react-native-async-storage/async-storage";
const user = AsyncStorage.getItem("user");
const initialState = user
  ? { isLoggedIn: true, user }
  : { isLoggedIn: false, user: null };
export default auth = (state = initialState, action) => {
  const { type, payload } = action;
switch (type) {
    case LOGIN_SUCCESS:
      console.log('callled')
      return {
        ...state,
        isLoggedIn: true,
        user: payload.user,
      };
      case LOGIN_FAILED:
        return {
          ...state,
          isLoggedIn: false,
          user: null,
        };
case LOGOUT:
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    default:
      return state;
  }
};

store.js

import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import basketReducer from "./features/basketSlice";
import restaurantReducer from "./features/restaurantSlice";
import productReducer from "./features/productSlice";
import authReducer from "./features/authSlice";
import thunk from "redux-thunk";

export const store = configureStore({
  reducer: {
    basket: basketReducer,
    restaurant: restaurantReducer,
    product: productReducer,
    auth: authReducer
  },
  middleware: [thunk, ...getDefaultMiddleware()],
})

错误原因及修复方案

核心原因

Redux检测到状态突变是因为authSlice.js中直接将异步操作的Promise对象赋值给了初始状态的user字段。AsyncStorage.getItem("user")返回的是Promise,而非实际用户数据,Redux追踪状态时,Promise内部属性(如_2)被修改,触发了状态突变警告。

此外还有几处逻辑问题需要同步修复:

1. 修复authSlice的初始状态与用户初始化逻辑

import { LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT } from "../actions/type";
import AsyncStorage from "@react-native-async-storage/async-storage";

// 初始状态默认未登录,用户数据为null
const initialState = { isLoggedIn: false, user: null };

export default function auth(state = initialState, action) {
  const { type, payload } = action;
  switch (type) {
    case LOGIN_SUCCESS:
      console.log('called')
      return {
        ...state,
        isLoggedIn: true,
        user: payload.user,
      };
    case LOGIN_FAILED:
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    case LOGOUT:
      return {
        ...state,
        isLoggedIn: false,
        user: null,
      };
    default:
      return state;
  }
}

// 新增异步action用于启动时初始化用户状态
export const initializeAuth = () => async (dispatch) => {
  try {
    const userStr = await AsyncStorage.getItem("user");
    if (userStr) {
      const user = JSON.parse(userStr);
      dispatch({
        type: LOGIN_SUCCESS,
        payload: { user }
      });
    }
  } catch (error) {
    console.error("初始化用户状态失败", error);
  }
};

在App启动组件中调用初始化action:

import { useDispatch } from 'react-redux';
import { initializeAuth } from './features/authSlice';

function App() {
  const dispatch = useDispatch();
  
  useEffect(() => {
    dispatch(initializeAuth());
  }, [dispatch]);
  
  // 其他组件逻辑
}

2. 修复authService的登录数据一致性

登录成功时返回实际用户数据,而非邮箱:

const logIn = async (user) => {
    console.log("user info", user);
    const { email, password } = user;
    return fetch('https://myapi.com/api/orders/login.json', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json;charset=UTF-8', "Accept": "application/json" },
      body: JSON.stringify({
        "email": email,
        "password": password
      })
    })
    .then((response) => response.json())
    .then(async (responseJson) => {
      if(responseJson.json.error === false) {
        console.log('successfulll');
        const userData = responseJson.json.user;
        await AsyncStorage.setItem("user", JSON.stringify(userData));
        return {
          status: "success",
          message: "正在跳转至首页",
          user: userData,
        };
      } else {
        return {
          status: "error",
          message: "Wrong credentials"
        };
      }
    })
    .catch((error) => {
      return {
        status: "error",
        message: "API call failed"
      };
    });
  };

3. 修复auth action的Promise处理逻辑

移除无效的Promise.resolve()/Promise.reject()调用,正确传递Promise状态:

export const login = (user) => (dispatch) => {
  return AuthService.logIn(user).then(
    (response) => {
      console.log(response)
      if (response.status === "success") {
        dispatch({
          type: LOGIN_SUCCESS,
          payload: { user: response.user },
        });
        return response;
      }
      return Promise.reject(response.message);
    },
    (error) => {
      const message = error.toString();
      return Promise.reject(message);
    }
  );
};

4. 修复store的中间件配置

Redux Toolkit默认包含thunk,无需重复添加:

export const store = configureStore({
  reducer: {
    basket: basketReducer,
    restaurant: restaurantReducer,
    product: productReducer,
    auth: authReducer
  },
  middleware: getDefaultMiddleware()
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 10:37:03