简体   繁体   English

React Context API 状态更新导致无限循环

[英]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.每当authCtxsendRequeststatusdataerror发生变化时,组件都会重新渲染。 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM