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

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>
    </>
  );
};

解决方案

核心问题在于:

  1. formTimeValues是每次渲染通过getValues()获取的新对象,添加到依赖后会导致useEffect频繁触发
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 06:25:55