React-Router & useContext,无限重定向或重新渲染

React-Router & useContext, infinite Redirect or Rerender

我有一个 Web 应用程序,我已经开发了一年多了,并且进行了一些更改。前端是 react w/ react-router-dom 5.2 来处理导航,一个 service worker 来处理缓存、安装和 webpush 通知,然后后端是一个 Javalin 应用程序,它存在于 Jetty 之上。

我正在使用上下文 API 来存储一些会话详细信息。当您导航到我的应用程序时,如果您尚未登录,那么您的信息将不会存储在该上下文中,因此您将被重定向到 /login,这将开始该过程。 LoginLogout 组件只是重定向到处理身份验证工作流的外部 authserver,然后再重定向回另一个端点。

详情如下:

  1. 服务器代码中没有重定向到 /login,ProtectedRoute 代码肯定是这个问题的罪魁祸首。导航到 /login 导致无限重定向或无限重新呈现。
  2. 所有重定向服务器端都使用代码 302 临时执行。同样,其中 none 指向 /login
  3. 这个问题,正如我所追踪的那样,我认为与上下文本身有关。我对上下文进行了修改,现在我遇到了与以前不同的行为,当时我认为服务工作者是罪魁祸首。问题仍然是无限重定向或重新呈现,很难解决。
  4. 我知道服务器正在做它的一部分,/auth/check 端点一直在提供它应该提供的东西。

这是我的 ProtectedRoute 代码

import { Redirect, Route, useLocation } from "react-router-dom";
import PropTypes from "prop-types";
import React, { useContext, useEffect, useState } from "react";
import { AuthContext } from "../Contexts/AuthProvider";
import LoadingComponent from "components/Loading/LoadingComponent";
import { server } from "variables/sitevars";

export const ProtectedRoute = ({ component: Component, ...rest }) => {
  const { session, setSession } = useContext(AuthContext);
  const [isLoading, setLoading] = useState(true);
  const [isError, setError] = useState(false);
  const cPath = useLocation().pathname;

  //Create a checkAgainTime
  const getCAT = (currTime, expireTime) => {
    return new Date(
      Date.now() + (new Date(expireTime) - new Date(currTime)) * 0.95
    );
  };

  //See if it's time to check with the server about our session
  const isCheckAgainTime = (checkTime) => {
    if (checkTime === undefined) {
      return true;
    } else {
      return Date.now() >= checkTime;
    }
  };

  useEffect(() => {
    let isMounted = true;
    let changed = false;
    if (isMounted) {
      (async () => {
        let sesh = session;
        try {
          //If first run, or previously not logged in, or past checkAgain
          if (!sesh.isLoggedIn || isCheckAgainTime(sesh.checkAgainTime)) {
            //Do fetch
            const response = await fetch(`${server}/auth/check`);
            if (response.ok) {
              const parsed = await response.json();
              //Set Login Status
              if (!sesh.isLoggedIn && parsed.isLoggedIn) {
                sesh.isLoggedIn = parsed.isLoggedIn;
                sesh.webuser = parsed.webuser;
                sesh.perms = parsed.perms;
                if (sesh.checkAgainTime === undefined) {
                  //Set checkAgainTime if none already set
                  sesh.checkAgainTime = getCAT(
                    parsed.currTime,
                    parsed.expireTime
                  );
                }
                changed = true;
              }
              if (sesh.isLoggedIn && !parsed.isLoggedIn) {
                sesh.isLoggedIn = false;
                sesh.checkAgainTime = undefined;
                sesh.webuser = undefined;
                sesh.perms = undefined;
                changed = true;
              }
            } else {
              setError(true);
            }
          }
          if (changed) {
            setSession(sesh);
          }
        } catch (error) {
          setError(true);
        }
        setLoading(false);
      })();
    }
    return function cleanup() {
      isMounted = false;
    };
  }, []);

  if (isLoading) {
    return <LoadingComponent isLoading={isLoading} />;
  }

  if (session.isLoggedIn && !isError) {
    return (
      <Route
        {...rest}
        render={(props) => {
          return <Component {...props} />;
        }}
      />
    );
  }

  if (!session.isLoggedIn && !isError) {
    return <Redirect to="/login" />;
  }

  if (isError) {
    return <Redirect to="/offline" />;
  }

  return null;    
};

ProtectedRoute.propTypes = {
  component: PropTypes.any.isRequired,
  exact: PropTypes.bool,
  path: PropTypes.string.isRequired,
};

下面是Authprovider的使用。我也继续给 login/logout 一个不同的端点:

export default function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Suspense fallback={<LoadingComponent />}>
          <Route path="/login" exact component={InOutRedirect} />
          <Route path="/logout" exact component={InOutRedirect} />
          <Route path="/auth/forbidden" component={AuthPage} />
          <Route path="/auth/error" component={ServerErrorPage} />
          <Route path="/offline" component={OfflinePage} />
          <AuthProvider>
            <ProtectedRoute path="/admin" component={AdminLayout} />
          </AuthProvider>
        </Suspense>
      </Switch>
    </BrowserRouter>
  );
}

这是 AuthProvider 本身:

import React, { createContext, useState } from "react";
import PropTypes from "prop-types";

export const AuthContext = createContext(null);

import { defaultProfilePic } from "../../views/Users/UserVarsAndFuncs/UserVarsAndFuncs";

const AuthProvider = (props) => {
  const [session, setSesh] = useState({
    isLoggedIn: undefined,
    checkAgainTime: undefined,
    webuser: {
      IDX: undefined,
      LastName: "",
      FirstName: "",
      EmailAddress: "",
      ProfilePic: defaultProfilePic,
    },
    perms: {
      IDX: undefined,
      Name: "",
      Descr: "",
    },
  });

  const setSession = (newSession) => {
    setSesh(newSession);
  };

  return (
    <AuthContext.Provider value={{ session, setSession }}>
      {props.children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

AuthProvider.propTypes = {
  children: PropTypes.any,
};

更新:因为有人要求,这里是我的 login/logout 组件,其中包含建议的更改(与 ProtectedRoute 依赖项分开)

import React, { useEffect, useState } from "react";
import { Redirect, useLocation } from "react-router-dom";

//Components
import LoadingComponent from "components/Loading/LoadingComponent";
import { server } from "variables/sitevars";

//Component Specific Vars

export default function InOutRedirect() {
  const rPath = useLocation().pathname;
  const [isError, setError] = useState(false);
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true;
    if (isMounted) {
      (async () => {
        try {
          //Do fetch
          const response = await fetch(`${server}/auth/server/data`);
          if (response.ok) {
            const parsed = await response.json();
            if (rPath === "/login") {
              window.location.assign(`${parsed.LoginURL}`);
            } else if (rPath === "/logout") {
              window.location.assign(`${parsed.LogoutURL}`);
            }
          }
        } catch (error) {
          setError(true);
        }
      })();
      setLoading(false);
    }
    return function cleanup() {
      isMounted = false;
    };
  }, []);

  if (isLoading) {
    return <LoadingComponent />;
  }

  if (isError) {
    return <Redirect to="/offline" />;
  }
}

我怎样才能找到这个问题?

更新:我已经进行了进一步的故障排除,现在确信我使用上下文的方式出了问题,而且 service worker 实际上并没有在这个问题中发挥作用。我更新了 post 以反映这一点。

更新 2:我做了进一步的简化。可以肯定的是,在页面呈现重定向组件并重定向回登录之前,或者完全没有通过 setSession 更新上下文。

更新 3:我相信我发现了问题,不是积极的,但我认为它已经解决了。赏金已经提供,如果有人能解释为什么会这样,那就是你的了。

我不太确定是否已解决此问题。在我确定之前不会接受这个答案。但问题似乎是,我的 ProtectedRoute 中没有默认渲染路径。我更新了 ProtectedRoute 代码以包括:

return null;

这是我原来的 ProtectedRoute 中缺少的。如果没有默认呈现路径,/login 呈现路径(如果用户未登录且没有错误)是唯一会 return 的呈现路径。我不确定为什么。我希望它与 React 批处理状态更新和渲染的方式有关。使用 return null 此代码可以正常工作....敲木头。

问题似乎与您的无序条件有关。如果您没有登录但出现错误怎么办?没有默认渲染,这将导致应用程序停止。当用户尝试登录时,状态被触摸,并且在出错时,这将不匹配任何渲染。当您将 return 设为 null 时,它会首先呈现它,过一会儿它会匹配正确的条件和 return 那个。所以,你可以这样订购你的条件:

if (isLoading) {
  // return ...
}
if (isError) {
  // return ...
}
if (session.isLoggedIn) {
 // return ...
}
return <Redirect to="/login" />;

在这里,我们首先检查是否有任何错误,如果是,则重定向到错误页面。否则,路由到登录组件或重定向到登录页面。

我认为您的问题是您正在尝试重定向到服务器端路由。我运行之前进入过同样的问题。 React-router 正在获取路径并尝试在没有可用路由的客户端为您提供服务,并且它正在使用导致循环的默认路由。

要解决这个问题,请创建一个模拟服务器端路由的路由,然后在服务器端工作流程完成后使用 react-router 在客户端重定向。

编辑:悬念标签应该在你的 switch iirc 之外。我会包括一个指向 404 或类似的默认路由。

这里我天真地认为Switch只允许RouteRedirect作为直接children,或者至少是path的组件将被视为路线。所以我不知道你的任何路由是如何工作的,对我来说它看起来像 Suspense 将总是被加载,然后所有内容都将被视为 children of Suspense 之后它只会加载并呈现所有路线。

除此之外,您真的不需要做 let isMounted = true 之类的事情,您使用 useEffect 的事实意味着 它已安装 ,你不必询问或告诉它或清理或任何东西,它已安装,你可以知道,useEffect 中的任何内容只会在组件安装时执行,而无论你 return from useEffect 将在组件卸载时执行。

除此之外,您真的不应该将身份验证上下文放在路由内,它是业务逻辑,属于外部,在您的页面路由之外。登录也不应该 'redirect' 某个地方的用户,它应该只访问 auth 上下文并更新状态,这应该自动从你的 auth 上下文重新呈现树,不需要手动重定向。将 auth 上下文放在 App 之类的地方,然后将路由器导入为它的 child。