reactjsreact-hook-form

Integrate RHF into MUI Autocomplete


I have a component TimePickerOwn which consists of two subcomponents which represent 2 MUI Autocompletes. This component acts as a TimePicker which contains different times with 30 minute interval in select dropdown, also arrows on each side can be clicked to add/subtract 30 minutes and manual text input can be done as well which on blur formats time to keep it in 30 min intervals. If user enter 12:33, on blur it will be formatted to 12:30, for example.

Time options are implemented by using an array of options and its index to move in it and place a value in the input. Before RHF I just used useEffect with index in deps to listen for changes and format input but after introducing RHF it doesn't work because I need to send this formatted value to RHF state.

Problem I have right now is that my useEffect doesn't work because it doesn't control state of RHF vale and I don't know how to refactor it so it does.

Basically I need a way to always compare values of both inputs and if a condition is met change them.

    const TimePickerOwn = () => {
      const { t } = useTranslation();
      const { setValue, getValues } = useFormContext();

  const formTimeValues = getValues()?.defaultDeliveryWindow;       
      const { defaultDeliveryWindow } = useSelector(
        (state) => state.orderIntake.currentOption
      );
    
      const defaultEarliestLoadTime =
        formTimeValues?.defaultEarliestLoadTime ||
        defaultDeliveryWindow?.defaultEarliestLoadTime;
    
      const defaultLatestLoadTime =
        formTimeValues?.defaultLatestLoadTime ||
        defaultDeliveryWindow?.defaultLatestLoadTime;
    
      const [timeEarliest, setTimeEarliest] = useState(
        findClosestTime(defaultEarliestLoadTime)
      );
      const [earliestIndex, setEarliestIndex] = useState(
        findCurrentIndex(timeEarliest)
      );
    
      const [timeLatest, setTimeLatest] = useState(
        findClosestTime(defaultLatestLoadTime)
      );
      const [latestIndex, setLatestIndex] = useState(findCurrentIndex(timeLatest));
    
      const timeOptionsLastIndex = timeOptions.length - 1;
    
      useEffect(() => {  //this useEffect doesn't change RHF value
          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, timeOptionsLastIndex]);
    
      useEffect(() => {
        setEarliestIndex(findCurrentIndex(defaultEarliestLoadTime));
        setLatestIndex(findCurrentIndex(defaultLatestLoadTime));
      }, [defaultEarliestLoadTime, defaultLatestLoadTime]);
    
      return (
        <>
          <TimeInput
            label={t("orderIntake.earliest")}
            id="startTime"
            time={timeEarliest}
            setTime={setTimeEarliest}
            index={earliestIndex}
            setIndex={setEarliestIndex}
            options={timeOptions}
            registerName="defaultDeliveryWindow.defaultEarliestLoadTime"
          />
          <Box marginBottom={2} />
          <TimeInput
            label={t("orderIntake.latest")}
            id="endTime"
            time={timeLatest}
            setTime={setTimeLatest}
            index={latestIndex}
            setIndex={setLatestIndex}
            options={timeOptions.slice(earliestIndex + 1)}
            registerName="defaultDeliveryWindow.defaultLatestLoadTime"
          />
        </>
      );
    };
    
    export default TimePickerOwn;

And here's the input:

const TimeInput = ({
      label = "time",
      time,
      setTime,
      id,
      setIndex,
      index,
      options,
      registerName,
    }) => {
      const { control } = useFormContext();
    
      const addTime = (field) => {
        setIndex((prev) => {
          const newIndex = prev >= timeOptions.length - 1 ? prev : prev + 1;
          field.onChange(time);
          return newIndex;
        });
      };
    
      const subtractTime = (field) => {
        setIndex((prev) => {
          const newIndex = prev <= 0 ? prev : prev - 1;
          field.onChange(time);
          return newIndex;
        });
      };
    
      const handleTextInput = (timeString) => {
        const closestTime = findClosestTime(timeString);
        setIndex(findCurrentIndex(closestTime));
        setTime(closestTime);
      };
    
      return (
        <Box sx={{ display: "flex", gap: "20px" }}>
          <Controller
            name={registerName}
            control={control}
            defaultValue={time}
            render={({ field }) => (
              <Autocomplete
                id={`oi-delivery-form-${id}`}
                freeSolo
                options={options}
                inputValue={field.value}
                filterOptions={(option) => option}
                onInputChange={(_, value) => field.onChange(addSemicolon(value))}
                onBlur={(e) => handleTextInput(e.target.value)}
                onChange={(_, value) => {
                  if (value) handleTextInput(value.label24h);
                }}
                getOptionLabel={(option) => option.label24h}
                renderInput={(params) => (
                  <TextField
                    {...params}
                    label={label}
                    variant="filled"
                    sx={{ width: "100%" }}
                    InputProps={{
                      ...params.InputProps,
                      style: { padding: 0, alignItems: "stretch" },
                      startAdornment: (
                        <ButtonWithTooltip
                          tooltipText={false}
                          onClick={() => subtractTime(field)}
                        >
                          <KeyboardArrowLeftIcon />
                        </ButtonWithTooltip>
                      ),
                      endAdornment: (
                        <ButtonWithTooltip
                          tooltipText={false}
                          onClick={() => addTime(field)}
                        >
                          <KeyboardArrowRightIcon />
                        </ButtonWithTooltip>
                      ),
                    }}
                    inputProps={{
                      ...params.inputProps,
                      maxLength: 5,
                      style: {
                        textAlign: "center",
                        width: "100%",
                        padding: "25px 0 8px 0",
                      },
                    }}
                    InputLabelProps={{
                      ...params.InputLabelProps,
                      style: {
                        top: "8%",
                        left: "50%",
                        transform: "translate(-50%, 0)",
                        fontSize: "12px",
                      },
                    }}
                  />
                )}
              />
            )}
          />
        </Box>
      );
    };
    
    export default TimeInput

Solution

  • Solved it by using watch from RHF. I listen for values change and assign formatted values when needed.

    const TimePickerOwn = () => {
      const { t } = useTranslation();
      const { getValues, watch } = useFormContext();
    
      const formTimeValues = getValues()?.defaultDeliveryWindow;
    
      const { defaultDeliveryWindow } = useSelector(
        (state) => state.orderIntake.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 [earliestIndex, setEarliestIndex] = useState(
        findCurrentIndex(watchEarliest)
      );
    
      const [latestIndex, setLatestIndex] = useState(findCurrentIndex(watchLatest));
    
      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 }, { name, type }) => {
          if (type) return;
          console.log(name, type);
          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}
            registerName="defaultDeliveryWindow.defaultEarliestLoadTime"
          />
          <Box marginBottom={2} />
          <TimeInput
            label={t("orderIntake.latest")}
            id="endTime"
            time={watchLatest}
            index={latestIndex}
            setIndex={setLatestIndex}
            options={timeOptions.slice(earliestIndex + 1)}
            registerName="defaultDeliveryWindow.defaultLatestLoadTime"
          />
        </>
      );
    };