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,然后再重定向回另一个端点。
详情如下:
- 服务器代码中没有重定向到 /login,ProtectedRoute 代码肯定是这个问题的罪魁祸首。导航到 /login 导致无限重定向或无限重新呈现。
- 所有重定向服务器端都使用代码 302 临时执行。同样,其中 none 指向 /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的使用。我也继续给 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
只允许Route
或Redirect
作为直接children,或者至少是path
的组件将被视为路线。所以我不知道你的任何路由是如何工作的,对我来说它看起来像 Suspense
将总是被加载,然后所有内容都将被视为 children of Suspense
之后它只会加载并呈现所有路线。
除此之外,您真的不需要做 let isMounted = true
之类的事情,您使用 useEffect
的事实意味着 它已安装 ,你不必询问或告诉它或清理或任何东西,它已安装,你可以知道,useEffect
中的任何内容只会在组件安装时执行,而无论你 return from useEffect
将在组件卸载时执行。
除此之外,您真的不应该将身份验证上下文放在路由内,它是业务逻辑,属于外部,在您的页面路由之外。登录也不应该 'redirect' 某个地方的用户,它应该只访问 auth 上下文并更新状态,这应该自动从你的 auth 上下文重新呈现树,不需要手动重定向。将 auth 上下文放在 App
之类的地方,然后将路由器导入为它的 child。
我有一个 Web 应用程序,我已经开发了一年多了,并且进行了一些更改。前端是 react w/ react-router-dom 5.2 来处理导航,一个 service worker 来处理缓存、安装和 webpush 通知,然后后端是一个 Javalin 应用程序,它存在于 Jetty 之上。
我正在使用上下文 API 来存储一些会话详细信息。当您导航到我的应用程序时,如果您尚未登录,那么您的信息将不会存储在该上下文中,因此您将被重定向到 /login,这将开始该过程。 LoginLogout 组件只是重定向到处理身份验证工作流的外部 authserver,然后再重定向回另一个端点。
详情如下:
- 服务器代码中没有重定向到 /login,ProtectedRoute 代码肯定是这个问题的罪魁祸首。导航到 /login 导致无限重定向或无限重新呈现。
- 所有重定向服务器端都使用代码 302 临时执行。同样,其中 none 指向 /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的使用。我也继续给 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
只允许Route
或Redirect
作为直接children,或者至少是path
的组件将被视为路线。所以我不知道你的任何路由是如何工作的,对我来说它看起来像 Suspense
将总是被加载,然后所有内容都将被视为 children of Suspense
之后它只会加载并呈现所有路线。
除此之外,您真的不需要做 let isMounted = true
之类的事情,您使用 useEffect
的事实意味着 它已安装 ,你不必询问或告诉它或清理或任何东西,它已安装,你可以知道,useEffect
中的任何内容只会在组件安装时执行,而无论你 return from useEffect
将在组件卸载时执行。
除此之外,您真的不应该将身份验证上下文放在路由内,它是业务逻辑,属于外部,在您的页面路由之外。登录也不应该 'redirect' 某个地方的用户,它应该只访问 auth 上下文并更新状态,这应该自动从你的 auth 上下文重新呈现树,不需要手动重定向。将 auth 上下文放在 App
之类的地方,然后将路由器导入为它的 child。