React:调用含Hook的函数时组件更新错误的解决方法
问题分析与解决方案
问题根源
你直接在TriggerDocumentCreationPage的渲染阶段调用TokenSupplier(),相当于把React组件当作普通函数执行。而TokenSupplier内部的acquireTokenSilent/acquireTokenPopup会触发MsalProvider的状态更新,违反了React规则:渲染阶段不能触发其他组件的状态更新,因此出现警告。同时你的TokenSupplier设计逻辑错误——它是异步获取令牌,但返回的是null,根本无法拿到令牌值。
以下是两种替代useContext的可行方案:
方案1:封装自定义Hook(推荐)
将令牌获取逻辑封装为符合React Hooks规则的自定义Hook,用状态管理令牌,在useEffect中执行异步操作,避免渲染阶段触发状态更新。
自定义Hook代码
import { useState, useEffect } from 'react'; import { InteractionRequiredAuthError } from "@azure/msal-browser"; import { useMsal } from "@azure/msal-react"; export function useAccessToken(scopes) { const [accessToken, setAccessToken] = useState(null); const [tokenError, setTokenError] = useState(null); const [isTokenLoading, setIsTokenLoading] = useState(true); const { instance, accounts } = useMsal(); useEffect(() => { const fetchToken = async () => { setIsTokenLoading(true); if (!accounts[0]) { setTokenError(new Error("未找到登录账户")); setIsTokenLoading(false); return; } const accessTokenRequest = { scopes, account: accounts[0], }; try { // 先尝试静默获取令牌 const response = await instance.acquireTokenSilent(accessTokenRequest); setAccessToken(response.accessToken); } catch (err) { if (err instanceof InteractionRequiredAuthError) { // 需要交互时弹出登录窗口 try { const response = await instance.acquireTokenPopup(accessTokenRequest); setAccessToken(response.accessToken); } catch (popupErr) { setTokenError(popupErr); } } else { setTokenError(err); } } finally { setIsTokenLoading(false); } }; fetchToken(); }, [instance, accounts, scopes]); return { accessToken, tokenError, isTokenLoading }; }
组件中使用
import { useState } from 'react'; import { useAccessToken } from '../hooks/useAccessToken'; function TriggerDocumentCreationPage() { const [isLoading, setIsLoading] = useState(false); // 传入所需的scope const { accessToken, tokenError, isTokenLoading } = useAccessToken([myScope]); // 在需要发起请求的事件中使用令牌(不要在渲染阶段直接发请求) const handleCreateDocument = async () => { if (!accessToken) return; setIsLoading(true); try { const response = await fetch('/api/document/create', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({/* 请求体 */}) }); // 处理响应 } catch (err) { console.error('请求失败:', err); } finally { setIsLoading(false); } }; // 处理令牌加载/错误状态 if (isTokenLoading) return <div>加载令牌中...</div>; if (tokenError) return <div>令牌获取失败: {tokenError.message}</div>; return ( <button onClick={handleCreateDocument} disabled={isLoading}> {isLoading ? '创建中...' : '触发文档创建'} </button> ); }
方案2:高阶组件(HOC)
将令牌获取逻辑封装为高阶组件,给目标组件注入令牌相关状态,适合类组件或偏好HOC风格的场景。
HOC代码
import { useState, useEffect } from 'react'; import { InteractionRequiredAuthError } from "@azure/msal-browser"; import { useMsal } from "@azure/msal-react"; export function withAccessToken(WrappedComponent, scopes) { return function WithAccessToken(props) { const [accessToken, setAccessToken] = useState(null); const [tokenError, setTokenError] = useState(null); const [isTokenLoading, setIsTokenLoading] = useState(true); const { instance, accounts } = useMsal(); useEffect(() => { const fetchToken = async () => { setIsTokenLoading(true); if (!accounts[0]) { setTokenError(new Error("未找到登录账户")); setIsTokenLoading(false); return; } const accessTokenRequest = { scopes, account: accounts[0], }; try { const response = await instance.acquireTokenSilent(accessTokenRequest); setAccessToken(response.accessToken); } catch (err) { if (err instanceof InteractionRequiredAuthError) { try { const response = await instance.acquireTokenPopup(accessTokenRequest); setAccessToken(response.accessToken); } catch (popupErr) { setTokenError(popupErr); } } else { setTokenError(err); } } finally { setIsTokenLoading(false); } }; fetchToken(); }, [instance, accounts, scopes]); return ( <WrappedComponent {...props} accessToken={accessToken} tokenError={tokenError} isTokenLoading={isTokenLoading} /> ); }; }
组件中使用
import { useState } from 'react'; import { withAccessToken } from '../hocs/withAccessToken'; function TriggerDocumentCreationPage({ accessToken, tokenError, isTokenLoading }) { const [isLoading, setIsLoading] = useState(false); const handleCreateDocument = async () => { if (!accessToken) return; setIsLoading(true); try { const response = await fetch('/api/document/create', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({/* 请求体 */}) }); // 处理响应 } catch (err) { console.error('请求失败:', err); } finally { setIsLoading(false); } }; if (isTokenLoading) return <div>加载令牌中...</div>; if (tokenError) return <div>令牌获取失败: {tokenError.message}</div>; return ( <button onClick={handleCreateDocument} disabled={isLoading}> {isLoading ? '创建中...' : '触发文档创建'} </button> ); } // 用HOC包装组件,传入所需scope export default withAccessToken(TriggerDocumentCreationPage, [myScope]);
内容的提问来源于stack exchange,提问作者Duke
相关产品推荐
相关产品推荐

