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

自定义Hook状态更新未触发React组件重渲染问题排查

问题排查:Apollo自定义Hook加载状态不触发组件重渲染

问题描述

基于React、TypeScript和Apollo开发应用,使用Apollo内置缓存替代状态管理。自定义Hook useResource会先检查Apollo缓存,数据缺失时通过useQuery发起查询。但在ResourceGuard组件中使用该Hook时,控制台显示Hook的loading状态从true变为false,组件却未随之重渲染,始终无法展示加载状态。

自定义Hook代码

import {
  RESOURCE_QUERY,
  ResourceFragment,
  Resource
} from '@custom/services';
import { useApolloClient, useQuery } from '@apollo/client';
import { useDebugValue } from 'react';

export const useResource = ({ id }: { id: string }) => {
  const client = useApolloClient();
  const cached = client.readFragment({
    id: `Resource:${id}`,
    fragment: ResourceFragment,
    fragmentName: 'ResourceFields'
  });

  const { loading, error } = useQuery(RESOURCE_QUERY, {
    variables: {
      findOneResourceId: id
    },
    skip: !!cached || !id
  });

  // ** 调试用,输出见文末
  console.log('useResource', loading);

  return {
    data: cached,
    loading,
    error
  };
};

组件使用代码

import { isValidElement } from 'react';
import { useParams } from 'react-router';
import { useResource } from './use-resource';

export function ResourceGuard({ children }: { children: React.ReactNode }) {
  const { resourceId } = useParams<{ resourceId: string }>();
  const { data, error, loading } = useResource({
    id: resourceId
  });

   // ** 调试用,输出见文末
  console.log('ResourceGuard', loading);

  if (loading) {
    return <div>Loading...</div>
  }

  if (!loading && error) {
    return <div>Not found</div>
  }

  return isValidElement(children) ? children : <>{children}</>;
}

控制台输出

useResource true
useResource true
useResource true
useResource true
useResource false
useResource false
useResource false
ResourceGuard false
useResource false
useResource false
useResource false
useResource false
useResource false
useResource false
useResource false

问题原因分析

  1. 路由参数初始为undefined导致跳过查询:
    组件初始化时,useParams返回的resourceId可能为undefined,此时useResource中skip: !!cached || !id的条件成立(!id为true),useQuery被跳过,loading直接返回false,组件直接渲染children。当resourceId变为有效值后,useQuery开始执行,loading变为true,但组件重渲染可能被合并或延迟,导致该状态未被捕获。

  2. data仅依赖缓存,未关联查询结果:
    useResource返回的data仅来自缓存读取,未结合useQuery的返回值。查询过程中loading状态变化时,data无更新,可能导致React未触发组件重渲染。

  3. 组件重渲染被父组件拦截:
    若ResourceGuard被父组件用React.memo包裹且未正确传递依赖项,路由参数变化时组件可能未重渲染,无法响应loading状态变化。

解决方案

方案1:先校验路由参数有效性

在ResourceGuard中先判断resourceId是否存在,避免id为undefined时直接渲染children:

export function ResourceGuard({ children }: { children: React.ReactNode }) {
  const { resourceId } = useParams<{ resourceId: string }>();

  if (!resourceId) {
    return <div>Loading...</div>;
  }

  const { data, error, loading } = useResource({ id: resourceId });

  console.log('ResourceGuard', loading);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Not found</div>;
  }

  return isValidElement(children) ? children : <>{children}</>;
}

方案2:合并缓存与查询结果

修改useResource,将缓存数据和查询结果合并,确保查询过程中状态变化能触发组件重渲染:

export const useResource = ({ id }: { id: string }) => {
  const client = useApolloClient();
  const cached = client.readFragment({
    id: `Resource:${id}`,
    fragment: ResourceFragment,
    fragmentName: 'ResourceFields'
  });

  const { loading, error, data: queryData } = useQuery(RESOURCE_QUERY, {
    variables: { findOneResourceId: id },
    skip: !!cached || !id
  });

  // 合并缓存与查询结果
  const data = cached || queryData?.findOneResource;

  console.log('useResource', loading);

  return { data, loading, error };
};

方案3:检查父组件的重渲染拦截

若父组件使用了React.memo,确保传递给ResourceGuard的children或其他props能正确触发重渲染,或直接移除React.memo以保证路由参数变化时组件能及时更新。

内容的提问来源于stack exchange,提问作者Nurhak Altin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 01:27:12