如何 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.

存储状态的不同位置

  1. 您可以将 isAuthorized 存储在 Context 中,只要您知道它在您需要时可用即可。如果上下文 not 在您想知道用户是否被授权时可用(这在您的应用程序中似乎是这种情况),那么您 不能使用Context来存储isAuthorized(至少不是单独的)。

  2. 每次需要的时候都可以isAuthorized。然后 isAuthorized 在 fetch 响应之前不可用。您的应用程序现在处于什么状态?它是 notReady(可能)。您可以将状态 notReady 存储在某处,例如再次在上下文中。 (notReady 最初总是 true,所以只有当您明确说明时,您才知道该应用程序已准备就绪。)该应用程序可能会显示一个 Spinner,只要它是 notReady.

  3. 您可以将 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();
  });
}