You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 21:44:55