React组件中useEffect内调用GQL查询钩子触发错误的问题咨询
我有一个组件需要通过GQL查询从后端获取数据,使用state对象根据API返回值设置对应的键。组件会通过检查相关键是否为空来决定是否渲染。最初我采用顺序执行的方式:先初始化空字符串变量,再调用API,若有结果则将变量设为返回值。
后来我尝试使用useState,但下方代码触发了错误:Invalid hook call. Hooks can only be called inside of the body of a function component。项目中其他地方也有类似实现,因此排除版本不兼容问题。
const SomeComponent = React.memo<PropInterface>(({ someProp }): JSX.Element => { const [state, setState] = useState<StateObj>({ key1: '', key2: '', key3: '', }) useEffect(() => { if (someProp === someCondition) { const { data } = useSomeQuery(args) if (data) { setState(prev => ({...prev, key1: data.newVal})); } } }, [someProp]); ... rest of component inc two similar queries
我在组件中处理3个独立查询,每个查询对应state对象中的一个字符串值。我不清楚问题出在哪里,是否应该改用useRef?考虑到组件是否渲染依赖后端数据且为只读状态,不确定使用钩子是否有必要或益处,但我希望所有查询异步执行以避免阻塞,是否应该回到组件内顺序调用的方式?
问题根源
错误的核心是你在useEffect内部调用了useSomeQuery这个Hook。React的Hook规则明确要求:所有Hook必须直接在函数组件的顶层作用域调用,不能嵌套在useEffect、条件判断、循环等代码块里。不管是内置Hook还是自定义的GQL查询Hook,都必须遵守这个规则。
正确实现方式
针对你的3个独立GQL查询,推荐直接在组件顶层调用对应的查询Hook,然后根据查询结果控制渲染:
方式1:直接用查询结果控制渲染(更简洁)
如果不需要把查询结果存在自己的state里,直接用查询Hook返回的data、loading等状态来判断渲染逻辑即可,大部分GQL客户端都支持skip参数控制查询是否执行:
const SomeComponent = React.memo<PropInterface>(({ someProp }): JSX.Element => { // 直接在组件顶层调用3个查询Hook,根据条件控制是否执行 const { data: data1 } = useSomeQuery(args, { skip: someProp !== someCondition }); const { data: data2 } = useAnotherQuery(otherArgs, { skip: /* 第二个查询的触发条件 */ }); const { data: data3 } = useThirdQuery(thirdArgs, { skip: /* 第三个查询的触发条件 */ }); // 直接用查询返回的data判断是否渲染对应部分 return ( <div> {data1?.newVal && <div>{data1.newVal}</div>} {data2?.someVal && <div>{data2.someVal}</div>} {data3?.anotherVal && <div>{data3.anotherVal}</div>} </div> ); });
方式2:将查询结果同步到自定义state(特殊需求场景)
如果确实需要把查询结果存在自己维护的state里,依然要在组件顶层调用查询Hook,再通过useEffect监听结果变化更新state:
const SomeComponent = React.memo<PropInterface>(({ someProp }): JSX.Element => { const [state, setState] = useState<StateObj>({ key1: '', key2: '', key3: '', }); // 顶层调用查询Hook,用skip控制执行时机 const { data: data1 } = useSomeQuery(args, { skip: someProp !== someCondition }); const { data: data2 } = useAnotherQuery(otherArgs, { skip: /* 触发条件 */ }); const { data: data3 } = useThirdQuery(thirdArgs, { skip: /* 触发条件 */ }); // 监听查询结果变化,同步到state useEffect(() => { if (data1?.newVal) { setState(prev => ({ ...prev, key1: data1.newVal })); } }, [data1]); useEffect(() => { if (data2?.someVal) { setState(prev => ({ ...prev, key2: data2.someVal })); } }, [data2]); useEffect(() => { if (data3?.anotherVal) { setState(prev => ({ ...prev, key3: data3.anotherVal })); } }, [data3]); // 用state控制渲染 return ( <div> {state.key1 && <div>{state.key1}</div>} {state.key2 && <div>{state.key2}</div>} {state.key3 && <div>{state.key3}</div>} </div> ); });
其他疑问解答
- 不需要使用useRef:你的场景需要根据后端数据触发组件重渲染来控制显示,而useRef存储的值不会触发重渲染,完全不适用。
- 不要回到顺序调用方式:GQL查询Hook本身就是异步执行的,不会阻塞组件渲染,还能自动处理请求缓存、状态更新等逻辑,比手动调用API更高效易维护。
内容的提问来源于stack exchange,提问作者Person1

