如何 prevent/lock 函数从返回到另一个单独的异步调用已解决?
how to prevent/lock a function from returning until another separate async call has resolved?
我正在处理路由身份验证,并将身份验证状态存储在上下文中,以便其他 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
变量是否包含对象(用户已登录)或是否为空。
为了检查用户是否登录,我使用 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 路由并使用 API 对 setLoggedInUser(response)
的回应。所以使用 authed()
总是 returns false,即使 API 响应稍后将 loggedInUser
设置为现在 authed()
为 true 的某个对象值。显然这是一个竞争条件,但我不确定如何解决它。
是否有一个优雅的解决方案,我可以 'lock' authed()
从返回值直到 useEffect() 完全 'set up' loggedInUser
的状态?
我设想的一个(糟糕的)解决方案可能是这样的:
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()
}, [])
有没有更好的方法 'lock' 函数 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 中,试着想想
- “我的应用程序在任何给定时刻的状态是什么”,
- 而不是“应该按什么顺序发生什么”。
你的情况:
- "根据状态,现在应该显示哪个页面",
- 而不是“一有事情就重定向”。
用户获得授权或未获得授权在任何特定时刻使用您的应用程序。那是一个 state,你会把这个状态存储在某个地方。我们称此状态为 isAuthorized
.
存储状态的不同位置
您可以将 isAuthorized
存储在 Context 中,只要您知道它在您需要时可用即可。如果上下文 not 在您想知道用户是否被授权时可用(这在您的应用程序中似乎是这种情况),那么您 不能使用Context来存储isAuthorized
(至少不是单独的)。
每次需要的时候都可以取isAuthorized
。然后 isAuthorized
在 fetch 响应之前不可用。您的应用程序现在处于什么状态?它是 notReady
(可能)。您可以将状态 notReady
存储在某处,例如再次在上下文中。 (notReady
最初总是 true
,所以只有当您明确说明时,您才知道该应用程序已准备就绪。)该应用程序可能会显示一个 Spinner,只要它是 notReady
.
您可以将 isAuthorized
存储在例如浏览器存储(例如sessionStorage
)。它们可跨页面加载使用,因此您不必每次都获取状态。浏览器存储应该是同步的,但实际上我会将它们视为异步的,因为我读到的有关浏览器存储的内容并不能激发信心。
问题与解决方案
您要做的是将 isAuthorized
存储在 (1) 上下文中,并且 (2) 每次都获取它,因此您有 2 个状态,需要 同步。无论如何,您 do 需要在开始时至少获取 isAuthorized
一次,否则,该应用程序无法使用。所以你确实需要同步和状态(3)notReady
(或isReady
)。
同步状态在 React 中使用 useEffect
完成(或使用 'dependencies'),例如:
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 中不可能在这种意义上阻止代码执行。您可以在其他时间 执行代码,例如使用承诺。这又需要以稍微不同的方式思考。
你不能做:
function authed(){
blockExecutionBasedOnCondition
return !!loggedInUser
}
但你可以这样做:
function authed(){
return !!loggedInUser;
}
function executeAuthed(){
someConditionWithPromise.then( result => {
authed();
});
}
我正在处理路由身份验证,并将身份验证状态存储在上下文中,以便其他 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
变量是否包含对象(用户已登录)或是否为空。
为了检查用户是否登录,我使用 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 路由并使用 API 对 setLoggedInUser(response)
的回应。所以使用 authed()
总是 returns false,即使 API 响应稍后将 loggedInUser
设置为现在 authed()
为 true 的某个对象值。显然这是一个竞争条件,但我不确定如何解决它。
是否有一个优雅的解决方案,我可以 'lock' authed()
从返回值直到 useEffect() 完全 'set up' loggedInUser
的状态?
我设想的一个(糟糕的)解决方案可能是这样的:
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()
}, [])
有没有更好的方法 'lock' 函数 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 中,试着想想
- “我的应用程序在任何给定时刻的状态是什么”,
- 而不是“应该按什么顺序发生什么”。
你的情况:
- "根据状态,现在应该显示哪个页面",
- 而不是“一有事情就重定向”。
用户获得授权或未获得授权在任何特定时刻使用您的应用程序。那是一个 state,你会把这个状态存储在某个地方。我们称此状态为 isAuthorized
.
存储状态的不同位置
您可以将
isAuthorized
存储在 Context 中,只要您知道它在您需要时可用即可。如果上下文 not 在您想知道用户是否被授权时可用(这在您的应用程序中似乎是这种情况),那么您 不能使用Context来存储isAuthorized
(至少不是单独的)。每次需要的时候都可以取
isAuthorized
。然后isAuthorized
在 fetch 响应之前不可用。您的应用程序现在处于什么状态?它是notReady
(可能)。您可以将状态notReady
存储在某处,例如再次在上下文中。 (notReady
最初总是true
,所以只有当您明确说明时,您才知道该应用程序已准备就绪。)该应用程序可能会显示一个 Spinner,只要它是notReady
.您可以将
isAuthorized
存储在例如浏览器存储(例如sessionStorage
)。它们可跨页面加载使用,因此您不必每次都获取状态。浏览器存储应该是同步的,但实际上我会将它们视为异步的,因为我读到的有关浏览器存储的内容并不能激发信心。
问题与解决方案
您要做的是将 isAuthorized
存储在 (1) 上下文中,并且 (2) 每次都获取它,因此您有 2 个状态,需要 同步。无论如何,您 do 需要在开始时至少获取 isAuthorized
一次,否则,该应用程序无法使用。所以你确实需要同步和状态(3)notReady
(或isReady
)。
同步状态在 React 中使用 useEffect
完成(或使用 'dependencies'),例如:
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 中不可能在这种意义上阻止代码执行。您可以在其他时间 执行代码,例如使用承诺。这又需要以稍微不同的方式思考。
你不能做:
function authed(){
blockExecutionBasedOnCondition
return !!loggedInUser
}
但你可以这样做:
function authed(){
return !!loggedInUser;
}
function executeAuthed(){
someConditionWithPromise.then( result => {
authed();
});
}