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

如何实现带日志的自定义Hook封装useQuery/useMutation?

问题描述

需求

创建名为useLoggedAPI的自定义Hook,用于调用useQuery或useMutation,并添加请求/错误日志,在所有GQL API调用场景中复用。

已尝试实现

基于urql库编写了useLoggedAPI代码,调用时传入查询参数和Hook类型,指定调用useQuery或useMutation。

遇到的问题

调用时触发错误:React Hook "useLoggedQuery"被条件调用,React Hooks必须在每次组件渲染时按完全相同的顺序调用,询问是否可实现该需求,并寻求不依赖外部库的解决方案。

尝试实现的代码

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  useQuery,
  useMutation,
  AnyVariables,
  UseQueryArgs,
  DocumentInput,
  CombinedError,
  UseQueryResponse,
  UseMutationResponse,
} from 'urql';
import { logger } from '../../../../../logger/src';

type HookType = 'useQuery' | 'useMutation';

type ApiResponse<T extends HookType> = T extends 'useQuery'
  ? UseQueryResponse
  : UseMutationResponse;

type UseQueryParamType<
  TData = any,
  TVars extends AnyVariables = AnyVariables
> = UseQueryArgs<TVars, TData>;

type UseMutationParamType<
  TData = any,
  TVars extends AnyVariables = AnyVariables
> = DocumentInput<TData, TVars>;

type QueryParamsType = UseQueryParamType | UseMutationParamType;

const useLoggedAPI = <T extends HookType>(
  queryParams: QueryParamsType,
  hookType: T
): ApiResponse<T> => {
  let executeAPI: any;
  let APIName;
  let responseData;

  const handleError = (error: CombinedError | undefined) => {
    if (!error) return;
    if (error.graphQLErrors) {
      logger.error({
        event: 'api_account_graphQL_error',
        error: error.graphQLErrors,
        message: 'Account graphQL Error',
      });
      return;
    }
    if (error.networkError) {
      logger.error({
        event: 'api_account_network_error',
        error: error.networkError,
        message: 'Account network error',
      });
      return;
    }
    logger.error({
      event: 'api_account_error',
      error: error,
      message: 'Account error',
    });
  };

  switch (hookType) {
    case 'useQuery':
      APIName = 'Query';
      [responseData, executeAPI] = useQuery(queryParams as UseQueryParamType);
      handleError(responseData.error);
      break;

    case 'useMutation':
      APIName = 'Mutation';
      [responseData, executeAPI] = useMutation(
        queryParams as UseMutationParamType
      );
      handleError(responseData.error);
      break;

    default:
      throw new Error(`Invalid hook type specified: ${hookType}`);
  }

  const executeAPIWithLogging = async (params?: any) => {
    let result = undefined;
    try {
      if (executeAPI) {
        result = await executeAPI(params);
      }
    } catch (error: any) {
      handleError(error);
    }
    return result;
  };

  return [responseData, executeAPIWithLogging];
};

export default useLoggedAPI;

调用示例

const queryParams = { query: 'mockQuery', variables: { id: 1 } };
const responseData = { data: { name: 'Mock Name' } };   

const result = useLoggedAPI(queryParams, 'useQuery');

解决方案

错误原因

React Hook规则要求所有Hook必须在组件顶层无条件调用,不能放在条件语句(如switch、if)内。你的代码根据hookType在分支中调用不同的GQL Hook,属于条件调用,违反规则导致报错。

方案1:拆分独立日志Hook(兼容urql)

直接拆分出两个独立的Hook,复用相同的日志逻辑,避免条件调用:

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  useQuery,
  useMutation,
  AnyVariables,
  UseQueryArgs,
  DocumentInput,
  CombinedError,
  UseQueryResponse,
  UseMutationResponse,
} from 'urql';
import { logger } from '../../../../../logger/src';

// 复用的错误处理逻辑
const handleError = (error: CombinedError | undefined) => {
  if (!error) return;
  if (error.graphQLErrors) {
    logger.error({
      event: 'api_account_graphQL_error',
      error: error.graphQLErrors,
      message: 'Account graphQL Error',
    });
    return;
  }
  if (error.networkError) {
    logger.error({
      event: 'api_account_network_error',
      error: error.networkError,
      message: 'Account network error',
    });
    return;
  }
  logger.error({
    event: 'api_account_error',
    error: error,
    message: 'Account error',
  });
};

// 带日志的useQuery
export const useLoggedQuery = <TData = any, TVars extends AnyVariables = AnyVariables>(
  params: UseQueryArgs<TVars, TData>
): UseQueryResponse => {
  const [responseData, executeQuery] = useQuery(params);
  
  handleError(responseData.error);

  const executeWithLogging = async (vars?: TVars) => {
    let result;
    try {
      result = await executeQuery(vars);
    } catch (error: any) {
      handleError(error);
    }
    return result;
  };

  return [responseData, executeWithLogging];
};

// 带日志的useMutation
export const useLoggedMutation = <TData = any, TVars extends AnyVariables = AnyVariables>(
  params: DocumentInput<TData, TVars>
): UseMutationResponse => {
  const [responseData, executeMutation] = useMutation(params);
  
  handleError(responseData.error);

  const executeWithLogging = async (vars?: TVars) => {
    let result;
    try {
      result = await executeMutation(vars);
    } catch (error: any) {
      handleError(error);
    }
    return result;
  };

  return [responseData, executeWithLogging];
};

调用方式

// 查询示例
const queryParams = { query: 'mockQuery', variables: { id: 1 } };
const [queryResult, executeQuery] = useLoggedQuery(queryParams);

// 变更示例
const mutationDoc = `mutation UpdateName($id: ID!, $name: String!) { updateName(id: $id, name: $name) { id name } }`;
const [mutationResult, executeMutation] = useLoggedMutation(mutationDoc);

方案2:不依赖外部库的通用日志Hook(原生fetch实现)

完全脱离GQL库,用原生fetch实现带日志的通用API Hook:

import { useState, useCallback } from 'react';
import { logger } from '../../../../../logger/src';

type GQLRequestType = 'query' | 'mutation';

interface GQLRequest<TVars = any> {
  type: GQLRequestType;
  query: string;
  variables?: TVars;
  url: string; // GraphQL接口地址
}

interface GQLResponse<TData = any> {
  data?: TData;
  errors?: Array<{ message: string; extensions?: any }>;
}

const useLoggedGQLAPI = <TData = any, TVars = any>() => {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState<TData | null>(null);
  const [error, setError] = useState<Error | null>(null);

  // 通用错误处理
  const handleError = (err: Error | GQLResponse['errors']) => {
    if (Array.isArray(err)) {
      logger.error({
        event: 'api_account_graphQL_error',
        error: err,
        message: 'Account GraphQL Error',
      });
      setError(new Error(err.map(e => e.message).join(', ')));
    } else {
      logger.error({
        event: 'api_account_network_error',
        error: err,
        message: 'Account Network Error',
      });
      setError(err);
    }
  };

  const execute = useCallback(async (request: GQLRequest<TVars>) => {
    setLoading(true);
    setError(null);
    setData(null);

    try {
      const response = await fetch(request.url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          operationName: undefined,
          query: request.query,
          variables: request.variables,
        }),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result: GQLResponse<TData> = await response.json();

      if (result.errors) {
        handleError(result.errors);
        return result;
      }

      setData(result.data as TData);
      return result;
    } catch (err) {
      handleError(err as Error);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  return { loading, data, error, execute };
};

export default useLoggedGQLAPI;

调用方式

// 查询示例
const { loading, data, error, execute } = useLoggedGQLAPI();

const fetchData = async () => {
  await execute({
    type: 'query',
    url: 'https://your-gql-api.com/graphql',
    query: `query GetUser($id: ID!) { user(id: $id) { id name } }`,
    variables: { id: 1 },
  });
};

// 变更示例
const updateName = async () => {
  await execute({
    type: 'mutation',
    url: 'https://your-gql-api.com/graphql',
    query: `mutation UpdateName($id: ID!, $name: String!) { updateName(id: $id, name: $name) { id name } }`,
    variables: { id: 1, name: 'New Name' },
  });
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 20:54:55