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

Posted

技术标签:

【中文标题】React-Router & useContext,无限重定向或重新渲染【英文标题】:React-Router & useContext, infinite Redirect or Rerender 【发布时间】:2021-10-11 15:13:21 【问题描述】:

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

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

详情如下:

    在服务器代码中没有重定向到 /login 并且 ProtectedRoute 代码肯定是这个问题的罪魁祸首。导航到 /login 会导致无限重定向或无限重新呈现。 服务器端的所有重定向都是临时使用代码 302 执行的。同样,它们都没有指向 /login 我认为这个问题与上下文本身有关。我已经对上下文进行了修改,现在我遇到了与以前不同的行为,当时我认为服务人员是罪魁祸首。问题仍然是无限重定向或重新呈现,很难解决。 我知道服务器正在做它的一部分,并且 /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 的使用。我还继续给登录/注销一个不同的端点:

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,
;

更新:因为它被要求,这里是我的登录/注销组件,建议的更改(与 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" />;
  

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

更新:我已经完成了进一步的故障排除,现在确信我使用上下文的方式有问题,并且服务工作者实际上并没有在这个问题中发挥作用。我已经更新了帖子以反映这一点。

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

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

【问题讨论】:

为什么需要保护您的登录路径?在我看来,它应该是一条常规路线,重定向到第三方,然后重定向到您的路线。 请分享MWE @LiorPollak 我继续并分离了该逻辑,但问题仍然存在。页面在更新有状态上下文之前呈现重定向。 @TheFunk 在检查了您的问题的旧版本后,我明白为什么它会不断重新加载,但如果返回 null 或不从 ProtectedRoutes 组件返回最新版本,我看不到任何问题不会引起任何问题,请检查here 并指出我是否缺少任何东西 @Chandan 看起来是正确的。也许我的假设是错误的,或者缓存在欺骗我,但我一直在虔诚地删除缓存。 【参考方案1】:

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

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

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

【讨论】:

我认为您可能是正确的。您是否尝试过 OAuth 代码授权?查看我的应用程序的网络选项卡中的返回,看起来 react-router 正在尝试为我的 API 的回调 URL 提供服务。现在我正在考虑它,身份验证服务器重定向到回调 URL(应该在服务器端处理)实际上会在服务器之前访问浏览器。我将更改身份验证服务器的重定向 URL 并在 react-router 中创建一个新路由来测试它。 我的问题与 OAuth 代码授权有关。您提出的解决方案似乎与我用来解决问题的方法相同。 原来是这样!考虑服务器端路由和将 Suspense 移到 Switch 之外一起解决了这个问题!因此,这似乎是多个问题合二为一。【参考方案2】:

在这里我天真地认为Switch 只允许RouteRedirect 作为直接子级,或者至少带有path 的组件将被视为路由。所以我不知道你的任何路由是如何工作的,在我看来Suspense 将始终被加载,之后所有内容都将被视为Suspense 的子级,之后它将加载并渲染所有路由.

除此之外你真的不需要像let isMounted = true 这样的事情,你使用useEffect 的事实意味着它已安装,你不必问或告诉它或者清理什么的,它已经挂载了,你可以知道,useEffect 里面的任何东西都只会在组件挂载时执行,而你从useEffect 返回的任何东西都将在组件卸载时执行。

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

【讨论】:

我现在才看到这个。这是问题的一半,但我首先看到了 DadBot 的帖子。对不起。如果我早点看到这个,我会分给你一半的赏金。 很不幸,因为这里接受的答案不是正确的答案,你在大部分事情上苦苦挣扎的原因是因为你所有的路线都加载了。在我回答你之后,他还编辑并回答了这个问题,但这就是生活。 @Matriarx 我希望我能和你分享一些奖励!发布更新时我没有注意。很抱歉!【参考方案3】:

问题似乎与您的无序条件有关。如果您没有登录但出现错误怎么办?不会有默认渲染,这会导致应用程序停止。当用户尝试登录时,状态被触摸并且出现错误时,这将不匹配任何渲染。当您输入 return null 时,它会首先呈现它,一段时间后它会匹配正确的条件并返回它。因此,您可以订购您的条件,例如:

if (isLoading) 
  // return ...

if (isError) 
  // return ...

if (session.isLoggedIn) 
 // return ...

return <Redirect to="/login" />;

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

【讨论】:

【参考方案4】:

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

return null;

这在我原来的 ProtectedRoute 中丢失了。如果没有默认渲染路径,/login 渲染路径(如果用户未登录且没有错误)是唯一会返回的路径。我不确定为什么。我希望它与如何响应批次状态更新和渲染有关。使用return null,此代码正在运行....敲木头。

【讨论】:

以上是关于React-Router & useContext,无限重定向或重新渲染的主要内容,如果未能解决你的问题,请参考以下文章

React-router <Link> 没有活动类

使用 Jest 测试来自 react-router v4 的链接

React-router 多路由(React 组件)

React-router条件渲染

如何在 react-router 中为 Link 或 IndexLink 的包装元素设置 activeClassName?

React & Material-UI 分页 - 将 Material-UI 的组件连接到 React-Router