如何实现带日志的自定义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
相关产品推荐
相关产品推荐

