[英]React Context API state update leads to infinite loop
I am trying to add Authentication to my app and maintaining Auth State using React Context API.我正在尝试向我的应用程序添加身份验证并使用 React Context API 维护身份验证状态。
I am calling my api using a custom hook use-http.我正在使用自定义钩子 use-http 调用我的 api。
import { useCallback, useReducer } from 'react';
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function useHttp(requestFunction, startWithPending = false) {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = useCallback(
async requestData => {
dispatch({ type: 'SEND' });
try {
const responseData = await requestFunction(requestData);
dispatch({ type: 'SUCCESS', responseData });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
},
[requestFunction]
);
return {
sendRequest,
...httpState,
};
}
export default useHttp;
This is my Login page which calls the api and I need to navigate out of this page and also update my Auth Context.这是我的登录页面,它调用 api,我需要导航出此页面并更新我的身份验证上下文。
import { useCallback, useContext } from 'react';
import { makeStyles } from '@material-ui/core';
import Container from '@material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import useHttp from '../hooks/use-http';
import { login } from '../api/api';
import AuthContext from '../store/auth-context';
import { useEffect } from 'react';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
if (status === 'pending') {
console.log('making request');
}
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
if (status === 'completed' && error) {
console.log(error);
}
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
The login api -登录 api -
export const login = async ({ email, password }) => {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: email,
password: password,
},
};
const response = await axios(config);
return response.data;
};
The Auth Context -身份验证上下文 -
import React, { useState } from 'react';
import { useEffect, useCallback } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
let logoutTimer;
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const tokenData = retrieveStoredToken();
let initialToken = '';
if (tokenData) {
initialToken = tokenData.token;
}
const [token, setToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const logoutHandler = useCallback(() => {
setToken(null);
removeUser();
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
}
}, []);
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setToken(token);
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
logoutTimer = setTimeout(logoutHandler, remainingTime);
};
useEffect(() => {
if (tokenData) {
console.log(tokenData.duration);
logoutTimer = setTimeout(logoutHandler, tokenData.duration);
}
}, [tokenData, logoutHandler]);
const user = {
token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;
The problem is when I call loginHandler function of my AuthContext in Login Component, the Login component re-renders and this login function goes in an infinite loop.问题是当我在登录组件中调用我的 AuthContext 的 loginHandler 函数时,登录组件重新呈现并且这个登录函数进入无限循环。 What am I doing wrong?
我究竟做错了什么?
I am new to React and stuck on this issue since hours now.我是 React 的新手,几个小时以来一直在解决这个问题。
I think I know what it is.我想我知道它是什么。
You're bringing in a bunch of component state via hooks.您通过钩子引入了一堆组件状态。 Whenever
authCtx
, sendRequest
, status
, data
and error
change, the component re-renders.每当
authCtx
、 sendRequest
、 status
、 data
和error
发生变化时,组件都会重新渲染。 Avoid putting closures into the state.避免将关闭放入状态。 The closures trigger unnecessary re-renders.
闭包会触发不必要的重新渲染。
function Login() {
const authCtx = useContext(AuthContext);
const { sendRequest, status, data: userData, error } = useHttp(login);
Try looking for all closures that could be causing re-renders and make sure components don't depend on them.尝试查找可能导致重新渲染的所有闭包,并确保组件不依赖于它们。
Edit:编辑:
Ben West is right- you also have side effects happening during the render, which is wrong. Ben West 是对的——你在渲染过程中也会发生副作用,这是错误的。
When you have something like this in the body of a functional component:当您在功能组件的主体中有这样的东西时:
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
Change it to this:改成这样:
useEffect(() => {
if (status === 'completed' && userData) {
console.log('updateContext');
authCtx.login(userData);
}
}, [status, userData]); //the function in arg 1 is called whenever these dependencies change
I made a bunch of changes to your code:我对您的代码进行了大量更改:
It's down to 2 files.它减少到2个文件。 The other stuff I inlined.
我内联的其他东西。
I'm not that familiar with useContext(), so I can't say if you're using it correctly.我对 useContext() 不太熟悉,所以我不能说你是否正确使用它。
Login.js
: Login.js
:
import { useContext, useEffect } from 'react';
import { makeStyles } from '@material-ui/core';
import Container from '@material-ui/core/Container';
import LoginForm from '../components/login/LoginForm';
import AuthContext from '../store/auth-context';
const useStyles = makeStyles(theme => ({
pageWrapper: {
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
},
pageContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
flexGrow: '1',
},
}));
function httpReducer(state, action) {
switch (action.type) {
case 'SEND':
return {
data: null,
error: null,
status: 'pending',
};
case 'SUCCESS':
return {
data: action.responseData,
error: null,
status: 'completed',
};
case 'ERROR':
return {
data: null,
error: action.errorMessage,
status: 'completed',
};
default:
return state;
}
}
function Login() {
const [httpState, dispatch] = useReducer(httpReducer, {
status: startWithPending ? 'pending' : null,
data: null,
error: null,
});
const sendRequest = async requestData => {
dispatch({ type: 'SEND' });
try {
let config = {
method: 'post',
url: `${BACKEND_URL}/api/auth/`,
headers: { 'Content-Type': 'application/json' },
data: {
email: requestData.email,
password: requestData.password,
},
};
const response = await axios(config);
dispatch({ type: 'SUCCESS', responseData: response.data });
} catch (error) {
dispatch({
type: 'ERROR',
errorMessage: error.response.data.message || 'Something went wrong!',
});
}
};
const authCtx = useContext(AuthContext);
const loginHandler = (email, password) => {
sendRequest({ email, password });
};
useEffect(() => {
if (httpState.status === 'pending') {
console.log('making request');
}
}, [httpState.status]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.data) {
console.log('updateContext');
authCtx.login(httpState.data);
}
}, [httpState.status, httpState.data]);
useEffect(() => {
if (httpState.status === 'completed' && httpState.error) {
console.log(httpState.error);
}
}, [httpState.status, httpState.error]);
const classes = useStyles();
return (
<div className={classes.pageWrapper}>
<Container maxWidth="md" className={classes.pageContainer}>
<LoginForm status={httpState.status} onLoginHandler={loginHandler} />
</Container>
</div>
);
}
export default Login;
AuthContext.js
: AuthContext.js
:
import React, { useState } from 'react';
import { useEffect } from 'react';
import {
getUser,
removeUser,
saveUser,
getExpirationTime,
clearExpirationTime,
setExpirationTime,
} from '../utils/local-storage';
const AuthContext = React.createContext({
token: '',
isLoggedIn: false,
login: () => {},
logout: () => {},
});
const calculateRemainingTime = expirationTime => {
const currentTime = new Date().getTime();
const adjExpirationTime = new Date(expirationTime).getTime();
const remainingDuration = adjExpirationTime - currentTime;
return remainingDuration;
};
// is this asynchronous?
const retrieveStoredToken = () => {
const storedToken = getUser();
const storedExpirationDate = getExpirationTime();
const remainingTime = calculateRemainingTime(storedExpirationDate);
if (remainingTime <= 60) {
removeUser();
clearExpirationTime();
return null;
}
return {
token: storedToken,
duration: remainingTime,
};
};
export const AuthContextProvider = ({ children }) => {
const [tokenData, setTokenData] = useState(null);
const [logoutTimer, setLogoutTimer] = useState(null);
useEffect(() => {
const tokenData_ = retrieveStoredToken(); //is this asynchronous?
if (tokenData_) {
setTokenData(tokenData_);
}
}, []);
const userIsLoggedIn = !!(tokenData && tokenData.token);
const logoutHandler = () => {
setTokenData(null);
removeUser();//is this asynchronous?
clearExpirationTime();
if (logoutTimer) {
clearTimeout(logoutTimer);
//clear logoutTimer state here? -> setLogoutTimer(null);
}
};
const loginHandler = ({ token, user }) => {
console.log('login Handler runs');
console.log(token, user.expiresIn);
setTokenData({ token });
saveUser(token);
setExpirationTime(user.expiresIn);
const remainingTime = calculateRemainingTime(user.expiresIn);
setLogoutTimer(setTimeout(logoutHandler, remainingTime));
};
useEffect(() => {
if (tokenData && tokenData.duration) {
console.log(tokenData.duration);
setLogoutTimer(setTimeout(logoutHandler, tokenData.duration));
}
}, [tokenData]);
const user = {
token: tokenData.token,
isLoggedIn: userIsLoggedIn,
login: loginHandler,
logout: logoutHandler,
};
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
export default AuthContext;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.