React循环调用辅助函数致页面崩溃 Hook报错排查
问题定位
你遇到的两个报错本质都是违反React Hooks的核心调用规则:
- React要求所有Hook只能在函数组件/自定义Hook的顶层作用域调用,不能在循环、条件分支、普通工具函数、useEffect/useMemo等内置Hook的回调函数里调用
- 你写的普通工具函数
getVitalsFromObs内部直接调用了useConfig()这个Hook,本身就不符合Hook调用要求:- 最初你在组件渲染阶段的循环里调用这个工具函数,每次渲染循环执行的次数不固定,导致React记录的Hook调用顺序错乱,触发「Rendered fewer hooks than expected」报错
- 后续你把调用挪到
useEffect回调里,此时执行上下文不属于组件顶层,直接触发「Invalid hook call」报错
- 额外问题:你最初写的遍历逻辑直接放在组件渲染阶段、还在遍历里反复调用
setX,本身就会触发无限重渲染;而且你把[encounters]写在forEach的第二个参数位置,根本不是Hook的依赖数组,属于语法书写错误。
修复方案
1. 改造普通辅助函数
普通工具函数内部不要调用任何Hook,把函数依赖的config通过入参传入,保持纯逻辑属性:
import { formatTime, parseDate } from '@openmrs/esm-framework'; import type { ConfigObject, Observation, PatientVitals, Encounter } from './types'; // 普通工具函数只处理纯逻辑,依赖值通过参数传入 export const getVitalsFromObs = (enc: Encounter, config: ConfigObject): Array<PatientVitals> => { const vitals: Array<PatientVitals> = []; enc.obs?.forEach((obs: Observation) => { const providerInfo = enc.encounterProviders.length ? { name: enc.encounterProviders[0].provider.person.display, role: enc.encounterProviders[0].encounterRole.display, } : { name: '', role: '' }; const obsTime = formatTime(parseDate(obs.obsDatetime)); switch(obs.concept?.uuid) { case config.concepts.pulseUuid: vitals.push({ pulse: obs.value, provider: providerInfo, time: obsTime }); break; case config.concepts.oxygenSaturationUuid: vitals.push({ oxygenSaturation: obs.value }); break; case config.concepts.respiratoryRateUuid: vitals.push({ respiratoryRate: obs.value }); break; case config.concepts.temperatureUuid: vitals.push({ temperature: obs.value }); break; case config.concepts.systolicBloodPressureUuid: vitals.push({ systolic: obs.value }); break; case config.concepts.diastolicBloodPressureUuid: vitals.push({ diastolic: obs.value }); break; case config.concepts.weightUuid: vitals.push({ weight: obs.value }); break; case config.concepts.heightUuid: vitals.push({ height: obs.value }); break; } }); return vitals; };
2. 修正组件逻辑
在组件顶层合法调用useConfig拿到配置,派生的生命体征数据直接用useMemo缓存计算即可,不需要额外用useState存储+反复setState触发重渲染:
import { useMemo, useState } from 'react'; import { useConfig, useTranslation } from '@openmrs/esm-framework'; import { Tabs, Tab } from '@carbon/react'; import { getVitalsFromObs } from '../../helpers/vitals-helper'; import Vitals from './vitals.component'; import styles from './past-visit-summary.scss'; const PastVisitSummary: React.FC<PastVisitSummaryProps> = ({ encounters, patientUuid }) => { const { t } = useTranslation(); const config = useConfig() as ConfigObject; const [selectedTabIndex, setSelectedTabIndex] = useState(0); const isTablet = useLayoutType() === 'tablet'; // 直接用useMemo计算派生数据,依赖变化时自动重算 const allVitals = useMemo(() => { const vitalsList: Array<PatientVitals> = []; encounters?.forEach((encounterGroup) => { encounterGroup?.encounters?.forEach((encounter: Encounter) => { vitalsList.push(...getVitalsFromObs(encounter, config)); }); }); return vitalsList; }, [encounters, config]); return ( <div className={styles.wrapper}> <Tabs className={`${styles.verticalTabs} ${isTablet ? styles.tabletTabs : styles.desktopTabs}`}> <Tab className={`${styles.tab} ${styles.bodyLong01} ${selectedTabIndex === 0 && styles.selectedTab}`} id="vitals-tab" onClick={() => setSelectedTabIndex(0)} label={t('vitals', 'Vitals')}> <Vitals vitals={allVitals} /> </Tab> </Tabs> </div> ) }
注意事项
- 如果确实需要封装带Hook调用的可复用逻辑,请封装为自定义Hook,函数名以
use开头(比如useGetVitalsFromObs),内部可以调用其他Hook,同样只在组件顶层调用 - 能通过props/state直接计算得到的派生数据,不要额外存到
useState里反复set,优先用useMemo做缓存,避免不必要的重渲染和状态同步问题
内容的提问来源于stack exchange,提问作者CKW
相关产品推荐
相关产品推荐

