React时间选择器添加useEffect依赖后功能异常解决方案咨询
问题描述
开发了一款结合MUI标签页的时间选择器组件,支持「自定义时段」和「全天时段」切换:
- 切换到「全天」标签时,时间自动设为
00:00至23:59,输入框同步禁用 - 切回自定义模式时,恢复用户之前输入的时段
为监听标签切换状态编写了useEffect,未添加完整依赖时功能正常,但触发React Hook警告:
React Hook useEffect has missing dependencies: 'formTimeValues' and 'savedTime'. Either include them or remove the dependency array
原useEffect代码:
useEffect(() => { if (wholeDay) { setSavedTime(formTimeValues); setValue("defaultDeliveryWindow", wholeDayRange.defaultDeliveryWindow); } else { setValue("defaultDeliveryWindow", savedTime); } }, [setValue, wholeDay]);
但将formTimeValues和savedTime添加到依赖后,出现时间反复重置的问题,功能完全失效。需要重构逻辑以维持原有功能并消除警告。
完整组件代码
const wholeDayRange = { defaultDeliveryWindow: { defaultEarliestLoadTime: "00:00", defaultLatestLoadTime: "23:59", }, }; const TimePickerOwn = ({ wholeDay = false }) => { const { t } = useTranslation(); const { getValues, watch, setValue } = useFormContext(); const formTimeValues = getValues()?.defaultDeliveryWindow; const currentOption = useSelector((state) => state.orderIntake.currentOption); const { defaultDeliveryWindow } = currentOption; const defaultEarliestLoadTime = formTimeValues?.defaultEarliestLoadTime || defaultDeliveryWindow?.defaultEarliestLoadTime; const defaultLatestLoadTime = formTimeValues?.defaultLatestLoadTime || defaultDeliveryWindow?.defaultLatestLoadTime; const watchEarliest = watch( "defaultDeliveryWindow.defaultEarliestLoadTime", defaultEarliestLoadTime ); const watchLatest = watch( "defaultDeliveryWindow.defaultLatestLoadTime", defaultLatestLoadTime ); const [savedTime, setSavedTime] = useState({ defaultEarliestLoadTime, defaultLatestLoadTime, }); const [earliestIndex, setEarliestIndex] = useState( findCurrentIndex(watchEarliest) ); const [latestIndex, setLatestIndex] = useState(findCurrentIndex(watchLatest)); useEffect(() => { if (wholeDay) { setSavedTime(formTimeValues); setValue("defaultDeliveryWindow", wholeDayRange.defaultDeliveryWindow); } else { setValue("defaultDeliveryWindow", savedTime); } }, [setValue, wholeDay]); useEffect(() => { const timeOptionsLastIndex = timeOptions.length - 1; if (earliestIndex === latestIndex) { if (latestIndex === timeOptionsLastIndex) { setEarliestIndex(timeOptionsLastIndex - 1); } else { setLatestIndex((prev) => prev + 1); } } else if (earliestIndex > latestIndex) { setLatestIndex(earliestIndex + 1); } else if (latestIndex === 0) { setLatestIndex(earliestIndex + 1); } }, [earliestIndex, latestIndex]); useEffect(() => { const subscription = watch(({ defaultDeliveryWindow }, { type }) => { if (type) return; setEarliestIndex( findCurrentIndex(defaultDeliveryWindow.defaultEarliestLoadTime) ); setLatestIndex( findCurrentIndex(defaultDeliveryWindow.defaultLatestLoadTime) ); }); return () => subscription.unsubscribe(); }, [watch]); return ( <> <TimeInput label={t("orderIntake.earliest")} id="startTime" time={watchEarliest} index={earliestIndex} setIndex={setEarliestIndex} options={timeOptions.slice(0, -1)} registerName="defaultEarliestLoadTime" isDisabled={wholeDay} /> <Box marginBottom={2} /> <TimeInput label={t("orderIntake.latest")} id="endTime" time={watchLatest} index={latestIndex} setIndex={setLatestIndex} options={timeOptions.slice(earliestIndex + 1)} registerName="defaultLatestLoadTime" isDisabled={wholeDay} /> </> ); };
标签页组件代码
const OITimeTabs = () => { const { t } = useTranslation(); const [tabsValue, setTabsValue] = useState(0); const handleChange = (_, newValue) => { setTabsValue(newValue); }; return ( <> <Box sx={{ width: "100%", my: 2 }}> <StyledTabs onChange={handleChange} value={tabsValue} aria-label="Tabs to choose between whole day and time range delivery" TabIndicatorProps={{ hidden: true }} > <StyledTab label={t("orderIntake.timePicker.timeRange")} {...a11yProps(0)} /> <StyledTab label={t("orderIntake.wholeDay")} {...a11yProps(1)} /> </StyledTabs> <TimePickerOwn wholeDay={!!tabsValue} /> </Box> <Typography sx={{ fontSize: "14px", color: "#00374d", mb: 1 }}> {`${t("orderIntake.timePicker.note")}: ${t( "orderIntake.timePicker.noteDescription", { from: "01:00 PM", to: "11:59 PM", } )}`} </Typography> </> ); };
解决方案
核心问题在于:
formTimeValues是每次渲染通过getValues()获取的新对象,添加到依赖后会导致useEffect频繁触发savedTime是组件内部状态,修改它会触发组件重渲染,形成循环
提供两种可行的重构方案:
方案一:本地缓存+函数式更新(无需调整组件结构)
用useRef缓存formTimeValues避免不必要的依赖触发,同时使用setSavedTime的函数式更新获取最新状态,最后安全忽略非必要的依赖警告:
const TimePickerOwn = ({ wholeDay = false }) => { // ... 其他原有代码 // 新增ref缓存formTimeValues const formTimeValuesRef = useRef(formTimeValues); useEffect(() => { formTimeValuesRef.current = formTimeValues; }, [formTimeValues]); // 重构后的切换逻辑 useEffect(() => { if (wholeDay) { // 用ref获取最新的formTimeValues,避免添加到依赖 setSavedTime(formTimeValuesRef.current); setValue("defaultDeliveryWindow", wholeDayRange.defaultDeliveryWindow); } else { // 用函数式更新获取最新savedTime,避免添加到依赖 setSavedTime(prevSaved => { setValue("defaultDeliveryWindow", prevSaved); return prevSaved; }); } }, [setValue, wholeDay]); // 安全忽略警告:明确知道formTimeValues和savedTime无需触发此effect // eslint-disable-next-line react-hooks/exhaustive-deps // ... 其他原有代码 };
方案二:状态上移到父组件(更符合React最佳实践)
将时段切换的状态管理逻辑移到父组件OITimeTabs,子组件仅负责渲染和本地索引管理,彻底避免循环触发:
修改父组件OITimeTabs
const OITimeTabs = () => { const { t } = useTranslation(); const { getValues, setValue } = useFormContext(); const [tabsValue, setTabsValue] = useState(0); // 父组件保存自定义时段状态 const [savedCustomTime, setSavedCustomTime] = useState(null); const handleChange = (_, newValue) => { const isWholeDay = !!newValue; if (isWholeDay) { // 切换到全天时,保存当前自定义时段 const currentTime = getValues()?.defaultDeliveryWindow; setSavedCustomTime(currentTime); // 设置全天时段 setValue("defaultDeliveryWindow", { defaultEarliestLoadTime: "00:00", defaultLatestLoadTime: "23:59", }); } else { // 切回自定义时,恢复保存的时段 if (savedCustomTime) { setValue("defaultDeliveryWindow", savedCustomTime); } } setTabsValue(newValue); }; // ... 原有渲染代码不变 };
简化子组件TimePickerOwn
删除子组件中的savedTime状态和对应的切换逻辑:
const TimePickerOwn = ({ wholeDay = false }) => { // ... 其他原有代码 // 删除savedTime相关的useState和useEffect // ... 其他原有代码(保留索引管理、监听逻辑等) };
方案对比
- 方案一适合快速修复,无需调整组件结构,仅通过缓存和函数式更新解决循环问题
- 方案二更符合React状态管理原则,将跨组件的状态逻辑集中到父组件,代码结构更清晰,维护成本更低
内容的提问来源于stack exchange,提问作者magrega
相关产品推荐
相关产品推荐

