英文:
redirecting by loginstatus from local.storage
问题
我尝试实现会话管理和私有路由到我的应用程序中,但出现了一些问题。我现在使用的解决方案似乎有bug。我的登录会将登录状态更新为true,并将其存储在localstorage中作为storedLoginStatus。然后我尝试检查此值,以便在刷新页面时保持我原来所在的页面,即使在刷新或更改语言后也是如此,这将刷新页面,但每次刷新都会将我发送到未登录的屏幕。
有人知道我该如何解决这个问题吗?
这是处理登录状态和设置它的Navbar上下文(它还处理项目选择,与此问题无关):
[NavbarContext代码]
index.js处理路由:
[index.js代码]
这是登录屏幕及其逻辑:
[LoginScreen代码]
我知道这有点混乱,但我需要尽快使其正常工作,之后我会整理它,我认为问题可能出现在PrivateRoutes中我正在重定向的outlet对象上,因为我在控制台记录了storedLoginStatus的值,它似乎是true,应该重定向到outlet,这应该是当前页面,而不是“未登录”的页面。
有什么建议吗?也许我应该实现会话状态并解决整个问题?
非常感谢你们的帮助。
英文:
im trying to implement session management and private routes into my application and for some reason the solution im using now is bugging in a way. my log in updates login status to true if logged in,and stores it in localstorage as storedLoginStatus then im trying to check this value when refreshing the page so it keeps me on the same page i was even after refresh or language change which refreshes the page but every refresh sends me to the not logged in screen.
anyone know how i can fix this?
here is PrivateRoutes.js which handles redirecting upon loginstatus:
import { Outlet, Navigate, useLocation } from "react-router-dom";
import { useContext, useEffect } from "react";
import { NavbarContext } from "../../NavbarContext";
const PrivateRoutes = () => {
const { loginStatus, setLoginStatus } = useContext(NavbarContext);
const storedLoginStatus = JSON.parse(localStorage.getItem("loginStatus"));
let location = useLocation();
useEffect(() => {
const storedLoginStatus = JSON.parse(localStorage.getItem("loginStatus"));
if (storedLoginStatus !== null) {
setLoginStatus(storedLoginStatus);
console.log("stored = " + storedLoginStatus);
}
}, [setLoginStatus]);
return storedLoginStatus ? <Outlet /> : <Navigate to="/*" />;
};
export default PrivateRoutes;
this is navbar context which handles the loginstatus and sets it (it also handles project seletion, not relevant to this question):
import React, { createContext, useState, useEffect } from "react";
import NavbarData from "./components/Navbar/NavbarData";
export const NavbarContext = createContext();
export const NavbarProvider = ({ children }) => {
const [loginStatus, setLoginStatus] = useState(false);
const [navbarData, setNavbarData] = useState([]);
const [selectedProjectIndex, setSelectedProjectIndex] = useState(0);
useEffect(() => {
const storedProjectIndex = localStorage.getItem("selectedProjectIndex");
const storedLoginStatus = localStorage.getItem("loginStatus");
if (storedProjectIndex !== null) {
const parsedIndex = parseInt(storedProjectIndex, 10);
if (
!isNaN(parsedIndex) &&
parsedIndex >= 0 &&
parsedIndex < NavbarData.length
) {
setSelectedProjectIndex(parsedIndex);
}
}
if (storedLoginStatus !== null) {
setLoginStatus(JSON.parse(storedLoginStatus));
}
}, []);
useEffect(() => {
localStorage.setItem(
"selectedProjectIndex",
selectedProjectIndex.toString()
);
localStorage.setItem("loginStatus", JSON.stringify(loginStatus));
setNavbarData(NavbarData[selectedProjectIndex]?.tabs || []);
}, [selectedProjectIndex, loginStatus]);
return (
<NavbarContext.Provider
value={{
navbarData,
selectedProjectIndex,
setSelectedProjectIndex,
setLoginStatus,
loginStatus,
}}
>
{children}
</NavbarContext.Provider>
);
};
index.js handles routing:
import { createRoot } from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import "./i18n";
import React from "react";
import MonitoringOverview from "./routes/MonitoringOverview";
import PerformanceQuality from "./routes/PerformanceQuality";
import Finance from "./routes/Finance";
import Navbar from "./components/Navbar/Navbar";
import "./App.css";
import LoginScreen from "./routes/LoginScreen/LoginScreen";
import UserConfig from "./routes/UserConfig/UserConfig.js";
import Simulation from "./routes/Simulation";
import Fraud from "./routes/Fraud";
import Deployment from "./routes/Deployment";
import Planning from "./routes/Planning";
import Settings from "./routes/Settings";
import PrivateRoutes from "./routes/Utils/PrivateRoutes";
import NotLogged from "./routes/Utils/NotLoggedIn";
const router = createBrowserRouter([
{
path: "/",
element: (
<NavbarProvider>
<LoginScreen />
</NavbarProvider>
),
},
{
path: "/*",
element: (
<NavbarProvider>
<NotLogged />
</NavbarProvider>
),
},
{
element: (
<>
<NavbarProvider>
<Navbar />
<PrivateRoutes />
</NavbarProvider>
</>
),
children: [
{
path: "monitoringoverview",
element: <MonitoringOverview />,
},
{
path: "performancequality",
element: <PerformanceQuality />,
},
{
path: "finance",
element: <Finance />,
},
{
path: "simulation",
element: <Simulation />,
},
{
path: "userconfig",
element: <UserConfig />,
},
{
path: "fraud",
element: <Fraud />,
},
{
path: "deployment",
element: <Deployment />,
},
{
path: "planning",
element: <Planning />,
},
{
path: "settings",
element: <Settings />,
},
//
{
path: "cashflowoverview",
element: <CashflowOverview />,
},
//Elections
{
path: "elections",
element: <Elections />,
},
{
path: "whatif",
element: <Whatif />,
},
//
{
path: "overview",
element: <Overview />,
},
{
path: "resourcemanagement",
element: <ResourceManagement />,
},
//SuperGaz
{
path: "energymonitoring",
element: <EnergyMonitoring />,
},
//
{
path: "monitoring",
element: <Monitoring />,
},
{
path: "inquiry",
element: <Inquiry />,
},
//
],
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
and this is the login screen with its logic:
import React, { useState, useContext } from "react";
import "./LoginScreen.css";
import { Navigate } from "react-router-dom";
import { NavbarContext } from "../../NavbarContext";
// i18n implementation
import { useTranslation } from "react-i18next";
import i18next from "i18next";
function LoginScreen() {
//Context Provider
// React States
const [errorMessages, setErrorMessages] = useState({});
const [isSubmitted, setIsSubmitted] = useState(false);
const { setLoginStatus } = useContext(NavbarContext);
const { t, i18n } = useTranslation();
const lngs = {
en: { nativeName: "English" },
fr: { nativeName: "French" },
};
const lng = i18next.language;
const changeLanguage = (lng) => {
i18n.changeLanguage(lng);
window.location.reload();
};
//Database for demo authentication
const database = [
{
username: "11111",
password: "1111111",
},
{
username: "1111",
password: "1111111",
},
];
const errors = {
uname: "invalid username",
pass: "invalid password",
};
//Submit handling
const handleSubmit = (event) => {
//Prevent page reload
event.preventDefault();
var { uname, pass } = document.forms[0];
// Find user login info
const userData = database.find((user) => user.username === uname.value);
// Compare user info
if (userData) {
if (userData.password !== pass.value) {
// Invalid password
setErrorMessages({ name: "pass", message: errors.pass });
} else {
setIsSubmitted(true);
setLoginStatus(true);
}
} else {
// Username not found
setErrorMessages({ name: "uname", message: errors.uname });
}
};
//Error messages handling
const renderErrorMessage = (name) =>
name === errorMessages.name && (
<div className="error">{errorMessages.message}</div>
);
//Login Form
const renderForm = (
<div className="form">
<form action="" onSubmit={handleSubmit}>
<div className="input-container">
<label for="inp" class="inp">
<input
type="text"
id="inp"
placeholder="&nbsp;"
name="uname"
required
/>
<span class="label">{t("login.labelUser")}</span>
<span class="focus-bg"></span>
</label>
{renderErrorMessage("uname")}
</div>
<div className="input-container">
<label for="inp" class="inp">
<input
id="inp"
placeholder="&nbsp;"
type="password"
name="pass"
required
/>
<span class="label">{t("login.password")}</span>
<span class="focus-bg"></span>
</label>
{renderErrorMessage("pass")}
</div>
<div className="button-container">
<input type="submit" value={t("login.submit")} />
</div>
</form>
</div>
);
return (
<div className="app">
<div className="login-form">
{Object.keys(lngs).length > 0 && (
<div className="languageSelectLogin">
<select
value={lng}
onChange={(e) => changeLanguage(e.target.value)}
className="languageButtonLogin"
>
{Object.keys(lngs).map((lngKey) => (
<option key={lngKey} value={lngKey}>
{lngs[lngKey].nativeName}
</option>
))}
</select>
</div>
)}
<img className="pangeaLogo" src={pangeaLogo} alt="pangeaLogo" />
<div className="title">{t("login.welcomeMessage")}</div>
<div className="project-name">{t("login.projectName")}</div>
<div className="login-please">{t("login.loginPlease")}</div>
{isSubmitted ? <Navigate to="/monitoringoverview" /> : renderForm}
</div>
</div>
);
}
export default LoginScreen;
i know its kind of a mess but i need to make it work soon and ill fix it up after it works, i think it might be the outlet object im redirecting to in PrivateRoutes since i console logged the value of storedLoginStatus and it seems to be true as it should and redirect to outlet which should be the current page and not the "not-logged-in" one.
any ideas? maybe i should implement session state and resolve this whole issue?
Thanks a lot in advance
答案1
得分: 1
# 问题
我在代码中看到的明显问题:
1. 你正在将每个路由与 `NavbarProvider` 组件包装在一起,因此它们各自具有独立的状态。
2. 这些 `NavbarProvider` 实例中的 `loginStatus` 最初是 `false`,因此在尝试访问受保护的路由时,将使用此值进行初始渲染。
3. 看起来你试图使用 localStorage 作为应用状态的真实来源,但它应该只是状态持久化以便稍后检索,例如在后续页面加载或应用挂载时。
4. `ProtectedRoutes` 不应该是尝试在 `NavbarProvider` 组件中设置身份验证状态的组件,它只应该读取它并相应地采取行动。
5. `ProtectedRoutes` 组件没有重定向到有效路径。
# 解决方案
在 `NavbarProvider` 中从 localStorage 初始化 `loginStatus` 状态。对索引状态执行相同操作。使用 `useMemo` 钩子来计算派生的 `navbarData` "state" 值。实际上,每当你发现自己编写了一个 `useState`/`useEffect` 钩子耦合时,应该改用 `useMemo` 钩子。
```jsx
export const NavbarProvider = ({ children }) => {
// 从 localStorage 初始化
const [loginStatus, setLoginStatus] = useState(() => {
return !!JSON.parse(localStorage.getItem("loginStatus"));
});
const [selectedProjectIndex, setSelectedProjectIndex] = useState(() => {
const storedProjectIndex = JSON.parse(
localStorage.getItem("selectedProjectIndex")
);
if (storedProjectIndex !== null) {
const parsedIndex = Number(storedProjectIndex);
if (
!isNaN(parsedIndex) &&
parsedIndex >= 0 &&
parsedIndex < NavbarData.length
) {
return parsedIndex;
}
}
return 0;
});
// 将本地状态更新持久化到 localStorage
useEffect(() => {
localStorage.setItem("selectedProjectIndex", selectedProjectIndex.toString());
}, [selectedProjectIndex]);
useEffect(() => {
localStorage.setItem("loginStatus", JSON.stringify(loginStatus));
}, [loginStatus]);
const navbarData = useMemo(() => {
return NavbarData[selectedProjectIndex]?.tabs || [];
}, [selectedProjectIndex]);
return (
<NavbarContext.Provider
value={{
navbarData,
selectedProjectIndex,
setSelectedProjectIndex,
setLoginStatus,
loginStatus,
}}
>
{children}
</NavbarContext.Provider>
);
};
更新 PrivateRoutes
以读取上下文中的 loginStatus
值并相应地进行重定向。重定向到有效路由,例如登录页面的 "/"
。
const PrivateRoutes = () => {
const { loginStatus } = useContext(NavbarContext);
return loginStatus ? <Outlet /> : <Navigate to="/" replace />;
};
export default PrivateRoutes;
重构路由,以便只渲染一个 NavbarProvider
并维护单个应用程序身份验证状态。
const NavbarProviderLayout = (
<NavbarProvider>
<Outlet />
</NavbarProvider>
);
const NavbarLayout = (
<>
<Navbar />
<PrivateRoutes />
</>
);
const router = createBrowserRouter([
{
element: <NavbarProviderLayout />,
children: [
{ path: "/", element: <LoginScreen /> },
{ path: "*", element: <NotLogged /> },
{
element: <NavbarLayout />,
children: [
{ path: "monitoringoverview", element: <MonitoringOverview /> },
{ path: "performancequality", element: <PerformanceQuality /> },
{ path: "finance", element: <Finance /> },
{ path: "simulation", element: <Simulation /> },
{ path: "userconfig", element: <UserConfig /> },
{ path: "fraud", element: <Fraud /> },
{ path: "deployment", element: <Deployment /> },
{ path: "planning", element: <Planning /> },
{ path: "settings", element: <Settings /> },
{ path: "cashflowoverview", element: <CashflowOverview /> },
// Elections
{ path: "elections", element: <Elections /> },
{ path: "whatif", element: <Whatif /> },
//
{ path: "overview", element: <Overview /> },
{ path: "resourcemanagement", element: <ResourceManagement /> },
// SuperGaz
{ path: "energymonitoring", element: <EnergyMonitoring /> },
//
{ path: "monitoring", element: <Monitoring /> },
{ path: "inquiry", element: <Inquiry /> },
],
},
],
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
英文:
Issues
The overt issues I see with the code:
- You are wrapping individual routes with the
NavbarProvider
component so they each have their own independent state. loginStatus
is initiallyfalse
in theseNavbarProvider
instances, so this is the value that will be used on the initial render when trying to access protected routes.- It appears you are trying to use localStorage as the source of truth for your app state when it should simply be where state is persisted to be later retrieved from, e.g. a later page load/app mounting.
ProtectedRoutes
shouldn't be the component trying to set the auth state in theNavbarProvider
component, it should only read it and act accordingly.- The
ProtectedRoutes
component doesn't redirect to a valid path.
Solution
Initialize the loginStatus
state from localStorage in NavbarProvider
. Do the same for the index state. Use the useMemo
hook to compute the derived navbarData
"state" value. In fact, just about any time you see that you've coded a useState
/useEffect
hook coupling you should use the useMemo
hook instead.
export const NavbarProvider = ({ children }) => {
// Initialize from localStorage
const [loginStatus, setLoginStatus] = useState(() => {
return !!JSON.parse(localStorage.getItem("loginStatus"));
});
const [selectedProjectIndex, setSelectedProjectIndex] = useState(() => {
const storedProjectIndex = JSON.parse(
localStorage.getItem("selectedProjectIndex")
);
if (storedProjectIndex !== null) {
const parsedIndex = Number(storedProjectIndex);
if (
!isNaN(parsedIndex) &&
parsedIndex >= 0 &&
parsedIndex < NavbarData.length
) {
return parsedIndex;
}
}
return 0;
});
// Persist local state updates to localStorage
useEffect(() => {
localStorage.setItem("selectedProjectIndex", selectedProjectIndex.toString());
}, [selectedProjectIndex]);
useEffect(() => {
localStorage.setItem("loginStatus", JSON.stringify(loginStatus));
}, [loginStatus]);
const navbarData = useMemo(() => {
return NavbarData[selectedProjectIndex]?.tabs || [];
}, [selectedProjectIndex]);
return (
<NavbarContext.Provider
value={{
navbarData,
selectedProjectIndex,
setSelectedProjectIndex,
setLoginStatus,
loginStatus,
}}
>
{children}
</NavbarContext.Provider>
);
};
Update PrivateRoutes
to read the loginStatus
context value and redirect accordingly. Redirect to a valid route, e.g. "/"
for the login page.
const PrivateRoutes = () => {
const { loginStatus } = useContext(NavbarContext);
return loginStatus ? <Outlet /> : <Navigate to="/" replace />;
};
export default PrivateRoutes;
Restructure the routes so there is only one rendered NavbarProvider
and single app auth state being maintained.
const NavbarProviderLayout = (
<NavbarProvider>
<Outlet />
</NavbarProvider>
);
const NavbarLayout = (
<>
<Navbar />
<PrivateRoutes />
</>
);
const router = createBrowserRouter([
{
element: <NavbarProviderLayout />,
children: [
{ path: "/", element: <LoginScreen /> },
{ path: "*", element: <NotLogged /> },
{
element: <NavbarLayout />,
children: [
{ path: "monitoringoverview", element: <MonitoringOverview /> },
{ path: "performancequality", element: <PerformanceQuality /> },
{ path: "finance", element: <Finance /> },
{ path: "simulation", element: <Simulation /> },
{ path: "userconfig", element: <UserConfig /> },
{ path: "fraud", element: <Fraud /> },
{ path: "deployment", element: <Deployment /> },
{ path: "planning", element: <Planning /> },
{ path: "settings", element: <Settings /> },
{ path: "cashflowoverview", element: <CashflowOverview /> },
// Elections
{ path: "elections", element: <Elections /> },
{ path: "whatif", element: <Whatif /> },
//
{ path: "overview", element: <Overview /> },
{ path: "resourcemanagement", element: <ResourceManagement /> },
// SuperGaz
{ path: "energymonitoring", element: <EnergyMonitoring /> },
//
{ path: "monitoring", element: <Monitoring /> },
{ path: "inquiry", element: <Inquiry /> },
],
},
],
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论