我正在进行路由身份验证,并将身份验证状态存储在上下文中,以便其他React组件可以检查用户是否登录
const [loggedInUser, setLoggedInUser] = useState(null)
const authed = () => !!loggedInUser
useEffect(() => {
async function fetchData() {
const response = await fetch('/loggedInUser')
.then(res => res.json())
setLoggedInUser(response)
}
fetchData()
}, [])
对此代码的快速解释是,我需要在代码的各个部分中包含用户数据(如id(,因此我需要存储loggedInUser
对象。然而,对于更简单的任务,例如检查用户是否登录,我使用函数authed
来检查loggedInUser
变量是否包含对象(用户已登录(或为null。
为了检查用户是否登录,我使用passport.js,我的'/loggedInUser'
路由如下所示:
app.get('/loggedInUser', async (req, res) => {
if (!req.isAuthenticated())
return null
const { id, name } = await req.user
.then(res => res.dataValues)
return res.json({ id, name })
})
问题是,在useEffect()
和fetch()
能够到达GET路由并使用对setLoggedInUser(response)
的API响应之前,消耗该上下文并检查authed()
的代码正在运行。所以authed()
的使用总是返回false,即使API响应后来将loggedInUser
设置为某个对象值,而现在authed()
为true。很明显,这是一个竞赛条件,但我不确定如何解决它。
有没有一个优雅的解决方案,在useEffect((完全"设置"loggedInUser
的状态之前,我可以"锁定"authed()
不返回值?
我设想的一个(糟糕的(解决方案可能是这样的:
const [loggedInUser, setLoggedInUser] = useState(null)
const isFinishedLoading = false // <--
function authed() {
while (!isFinishedLoading) {} // a crude lock
return !!loggedInUser
}
useEffect(() => {
async function fetchData() {
const response = await fetch('/loggedInUser')
.then(res => res.json())
setLoggedInUser(response)
isFinishedLoading = true // <--
}
fetchData()
}, [])
是否有更好的方法来"锁定"函数authed()
,直到加载完成?
编辑:
为了澄清我在评论中对authed()
的使用,这里是我的App.js
的精简版本
export default function App() {
return (
<>
<AuthProvider>
<Router className="Router">
<ProtectedRoute path="/" component={SubmittalTable} />
<Login path="/login" />
</Router>
</AuthProvider>
</>
)
}
function ProtectedRoute({ component: Component, ...rest }){
const { authed } = useContext(AuthContext)
if (!authed()) // due to race condition, authed() is always false
return (<Redirect from="" to="login" noThrow />)
return (<Component {...rest} />)
}
我不明白为什么需要authed()
作为函数,而不直接使用isLoggedIn
。此外,我看不出您正在设置上下文值,但无论如何。。。
一般性建议
一般来说:在React中,试着思考
- "我的应用程序在任何给定时刻的状态是什么
- 而不是";应该按照哪个顺序发生什么
在您的情况下:
- "基于状态">
- 而不是";一旦发生事情就重定向">
用户被授权或未被授权在任何特定时刻使用您的应用程序。这是一个状态,您可以将此状态存储在某个地方。让我们把这种状态称为isAuthorized
。
存储状态的不同位置
-
您可以将
isAuthorized
存储在Context中,只要您知道它在需要时可用。如果上下文在您想知道用户是否获得授权时不可用(在您的应用程序中似乎是这样(,那么您就不能使用上下文来存储isAuthorized
(至少不是单独使用(。 -
您可以在每次需要时提取
isAuthorized
。在提取响应之前,isAuthorized
不可用。你的应用程序现在的状态如何?可能是notReady
。您可以将状态notReady
存储在某个位置,例如再次存储在上下文中。(notReady
最初总是true
,所以只有明确表示应用程序已准备就绪。(只要是notReady
,应用程序可能会显示Spinner,而不执行任何其他操作。 -
您可以将
isAuthorized
存储在浏览器存储中(例如sessionStorage
(。它们可以跨页面加载,因此不必每次都获取状态。浏览器存储应该是同步的,但事实上,我会把它们视为异步的,因为我读到的关于浏览器存储的东西并不能激发人们的信心。
问题及解决方案
您要做的是将isAuthorized
存储在(1(上下文中,并(2(每次提取它,因此您有两个状态,需要同步。无论如何,确实需要在一开始至少获取isAuthorized
一次,否则,应用程序就无法使用。因此,您确实需要同步和状态(3(notReady
(或isReady
(。
使用React中的useEffect
(或"依赖项"(完成同步状态,例如:
useEffect(() => {
setIsFinishedLoading( false ); // state (3) "app is ready"
fetchData().then( response => {
setLoggedInUser( response ); // state (2) "isAuthorized" from fetch
setIsFinishedLoading( true );
}).catch( error => {
setLoggedInUser( null );
setIsFinishedLoading( true );
});
}, []);
useEffect(() => {
if( isFinishedLoading ){
setIsAuthorized( !!response ); // state (1) "isAuthorized" in local state or Context
}
}, [ isFinishedLoading, response ]);
"阻塞">
你可能根本不需要它,但是:
在Javascript中,阻止这种意义上的代码执行是不可能的。相反,您将在其他时间执行代码,例如使用Promises。这再次需要以稍微不同的方式思考。
你不能做:
function authed(){
blockExecutionBasedOnCondition
return !!loggedInUser
}
但你可以做到:
function authed(){
return !!loggedInUser;
}
function executeAuthed(){
someConditionWithPromise.then( result => {
authed();
});
}