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
只允许Route
或Redirect
作为直接子级,或者至少带有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,无限重定向或重新渲染的主要内容,如果未能解决你的问题,请参考以下文章
使用 Jest 测试来自 react-router v4 的链接
如何在 react-router 中为 Link 或 IndexLink 的包装元素设置 activeClassName?