我试图实现项目的身份验证部分,但在登录页面,当我单击登录按钮时,它总是显示此错误:
Uncaught Error: Invariant failed: A state mutation was detected between dispatches, in the path 'auth.access'. This may cause incorrect behavior. (https://redux.js.org/style-guide/style-guide#do-not-mutate-state)
at invariant (immutableStateInvariantMiddleware.ts:25:1)
at immutableStateInvariantMiddleware.ts:256:1
at Object.measureTime (utils.ts:10:1)
at Object.dispatch (immutableStateInvariantMiddleware.ts:249:1)
at dispatch (<anonymous>:1:55003)
at onClickLogIn (LogInPage.js:50:1)
at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1)
at invokeGuardedCallback (react-dom.development.js:4277:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js:4291:1)
按照它的建议,我试着在onClickLogIn()
函数中调试,但没有找到原因,我正在使用@reduxjs/toolkit,所以我认为它帮助我管理了reducer中的不变,然而,即使我改变了reducer中的代码,使其以不可变的方式显式地改变了一切。同样的错误一次又一次的出现。2有人能帮我看看原因吗?3非常感谢!4下面是我的代码片段:
store.js
import {configureStore} from "@reduxjs/toolkit";
import authReducer from './authSlice';
import requestReducer from './requestSlice';
import {getAccessToken, getRefreshToken, setAccessToken, setRefreshToken} from "../utils/token";
import * as apiCalls from '../api/apiCalls'
let preloadedState = {
auth: {
access: '',
refresh: '',
loggedIn: false,
},
request: {
entities: [],
}
};
const refreshToken = getRefreshToken();
if (refreshToken !== '') {
apiCalls.refresh({'refresh': refreshToken}).then(resp => {
setAccessToken(resp.data.access);
setRefreshToken(resp.data.refresh);
preloadedState.auth.access = getAccessToken();
preloadedState.auth.refresh = getRefreshToken();
preloadedState.auth.loggedIn = true;
apiCalls.setAuthHeader(resp.data.access);
console.log('detect previous logged in: ', preloadedState);
}).catch(err => {
console.log('no auth / expired token in localstorage');
});
}
const store = configureStore({
reducer: {
auth: authReducer,
request:requestReducer,
},
preloadedState: preloadedState
});
const updateAuth = (state) => {
const access = state.auth.access;
const refresh = state.auth.refresh;
setAccessToken(access);
setRefreshToken(refresh);
apiCalls.setAuthHeader(access);
console.log('detect change in auth.')
}
console.log(store.getState());
store.subscribe(() => {
updateAuth(store.getState());
});
export default store;
authSlice.js
import {createSlice} from "@reduxjs/toolkit";
import * as apiCalls from '../api/apiCalls'
const authSlice = createSlice({
name: 'auth',
initialState: null,
reducers: {
assignToken(state, action) {
return {
loggedIn: true,
access: action.payload.access,
refresh: action.payload.refresh,
};
},
logOutUser(state, action) {
return {
loggedIn: false,
access: '',
refresh: '',
};
},
}
});
export const {
assignToken,
logOutUser,
} = authSlice.actions
export default authSlice.reducer;
export function handleLogin(confidential) {
return async function handleLoginThunk(dispatch, getState) {
return apiCalls.getToken(confidential).then((response) => {
console.log('log in: ', response);
dispatch(assignToken({...response.data}));
}
).catch((err) => {
return Promise.reject(err.message);
});
}
};
export function handleRefresh(refresh) {
return async function handleRefreshThunk(dispatch, getState) {
apiCalls.refresh({'refresh': refresh}).then((response) => {
console.log('refresh token:', ...response);
dispatch(assignToken(...response.data));
}
).catch((err) => {
});
}
};
export function handleLogOut() {
return async function handleLogOutThunk(dispatch, getState) {
const refreshToken = getState().auth.refresh;
console.log('logout: ', refreshToken);
dispatch(logOutUser());
apiCalls.logout({refresh_token: refreshToken})
.then((response) => {
}).catch(error => {
});
}
};
LoginPage.js
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import {useDispatch, useSelector} from "react-redux"
import {createTheme, ThemeProvider} from '@mui/material/styles';
import {useEffect, useState} from "react";
import * as apiCalls from "../api/apiCalls";
import {clearErrorMessage, handleLogin, handleLogOut, handleRefresh, logInUser} from "../redux/authSlice";
import Copyright from "../components/Copyright";
import axios from "axios";
import {useNavigate} from "react-router-dom";
import {Alert} from "@mui/material";
import {getRefreshToken} from "../utils/token";
const theme = createTheme();
export default function SignInSide() {
const navigate = useNavigate();
const [apiCallPending, setApiCallPending] = useState(false);
const [apiError, setApiError] = useState('');
const [role, setRole] = useState('Staff');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const loggedIn = useSelector(state => state.loggedIn);
const errMsg = useSelector(state => state.message);
const dispatch = useDispatch();
const nextRole = () => {
return role === 'Staff' ? 'Patient' : 'Staff';
};
// useEffect(() => {
// if (loggedIn)
// navigate('/dashboard', {state: {from: '/login'}});
// });
const onClickChangeRole = () => {
setRole(nextRole());
}
const onClickLogIn = (event) => {
event.preventDefault();
setApiCallPending(true);
const data = {username: username, password: password};
dispatch(handleLogin(data)).then(response => {
console.log('Log In Successfully: ', response);
navigate('/dashboard', {state: {from: '/login'}});
}).catch(err => {
});
};
return (
<ThemeProvider theme={theme}>
<Grid container component="main" sx={{height: '100vh'}}>
<CssBaseline/>
<Grid
item
xs={false}
sm={4}
md={7}
sx={{
backgroundImage: 'url(https://images.unsplash.com/photo-1594824476967-48c8b964273f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjF8fGRvY3RvcnxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=400&q=60)',
backgroundRepeat: 'no-repeat',
backgroundColor: (t) =>
t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<Box
sx={{
my: 8,
mx: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{m: 1, bgcolor: 'secondary.main'}}>
<LockOutlinedIcon/>
</Avatar>
<Typography component="h1" variant="h5">
{role} Sign in
</Typography>
<Box component="form" noValidate sx={{mt: 1}}>
<TextField
margin="normal"
required
fullWidth
id="username"
label="Username"
name="username"
value={username}
autoComplete="username"
autoFocus
onChange={(event) => {
setUsername(event.target.value)
}}
/>
{role === 'Staff' && <TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
value={password}
onChange={(event) => {
setPassword(event.target.value)
}}
autoComplete="current-password"
/>}
{/*<FormControlLabel*/}
{/* control={<Checkbox value="remember" color="primary"/>}*/}
{/* label="Remember me"*/}
{/*/>*/}
{apiError !== '' && <Alert severity="error">{apiError}</Alert>}
<Button
type="submit"
fullWidth
variant="contained"
onClick={onClickLogIn}
sx={{mt: 3, mb: 2}}
>
Sign In
</Button>
<Button
onClick={onClickChangeRole}
fullWidth
variant="contained"
sx={{mb: 2}}
>
{`${nextRole()} Log In`}
</Button>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
<Copyright sx={{mt: 5}}/>
</Box>
</Box>
</Grid>
</Grid>
</ThemeProvider>
);
}
1条答案
按热度按时间vawmfj5a1#
是的,根据错误消息,您正在修改路径
state.auth.access
,令牌逻辑中的这一行显然正在执行此操作:*不要 * 改变现有的状态!而是分派一个包含新令牌的操作,并在Reducer中更新该状态。