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