Вопрос или проблема
Я использую Mui Autocomplete вместе с react-hook-form.
Теперь для отображения опций я хочу получать их с сервера, пока печатаю в Autocomplete, я также хотел бы дебаунсить ввод.
Теперь я покажу, как я создал свой Autocomplete вместе с компонентом Control.
export function FormAutocompleteField<
TOption extends { id: string | number; label: string },
TFieldValues extends FieldValues,
TName extends FieldPath<TFieldValues>,
>({
control,
name,
options,
loading,
label,
onInputChange,
debounceTime,
}: UseControllerProps<TFieldValues, TName> & {
options: TOption[];
loading?: boolean;
label?: string;
onInputChange?: (input: string) => void;
debounceTime?: number;
}) {
return (
<Controller
control={control}
name={name}
render={({ field, fieldState: { error } }) => {
const { value, onChange, ref, onBlur, name, disabled } = field;
return (
<Autocomplete
options={options}
value={options.find((option) => option.id === value) ?? null}
onChange={(_event, value) => onChange(value?.id)}
disabled={disabled}
loading={loading}
onInputChange={debounce(
(_event, value) => onInputChange?.(value),
debounceTime,
)}
onBlur={onBlur}
autoSelect
renderInput={(params) => (
<TextField
{...params}
inputRef={ref}
label={label}
placeholder={label}
name={name}
error={!!error?.message}
helperText={error?.message}
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</>
),
},
}}
/>
)}
/>
);
}}
/>
);
}
Когда я печатаю в автозаполнении, состояние ввода будет обновлено с дебаунсингом, и затем запрос с useQuery
будет автоматически повторяться.
export default function useJobOfferForm() {
const [customerName, setCustomerName] = useState(
customerOwner &&
`${customerOwner.contact.name} ${customerOwner.contact.surname}`,
);
const form = useForm<JobOfferSchema>({
defaultValues: {
status: "Created",
customerId: customerOwner?.id,
},
resolver: zodResolver(jobOfferSchema),
});
const customersQuery = useQuery({
queryKey: customersKey({
page: 0,
limit: 10,
filters: { byFullName: customerName },
}),
queryFn: async () => {
const res = await customerApi.getCustomers(0, 10, {
byFullName: customerName,
});
return res.data;
},
});
function onCustomerNameChange(name: string) {
setCustomerName(name);
}
return {
form,
customers: customersQuery.data?.content?.map((c) => ({
id: c.id,
label: `${c.contact.name} ${c.contact.surname}`,
})),
customersPending: customersQuery.isPending,
onCustomerNameChange,
};
}
export default function JobOfferForm({
defaultJobOffer,
customerOwner,
onSubmit,
onCancel,
error,
isPending,
}: {
defaultJobOffer?: CreateJobOfferDTO;
customerOwner?: CustomerDTO;
onSubmit: SubmitHandler<CreateJobOfferDTO>;
onCancel?: () => void;
error?: Error | null;
isPending?: boolean;
}) {
const {
form,
customers,
customersPending,
onCustomerNameChange,
} = useJobOfferForm();
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* ... */}
<FormAutocompleteField
control={form.control}
name="customerId"
label="Создатель задания (клиент)"
onInputChange={onCustomerNameChange}
loading={customersPending}
options={customers ?? []}
debounceTime={1000}
/>
</form>
);
}
Теперь проблема в том, что когда customerName
будет обновлен, компоненты будут перерисованы. Насколько я понял, проблема в том, что value
в компоненте Control
уже установлен на option
, когда компонент будет перерисован, он совпадет с одной из options
, и input
будет установлен на предыдущее значение.
Как я могу добиться такого поведения (дебаунсинг запросов) без нарушения текстового ввода в автозаполнении?
Ответ или решение
Для реализации функции дебаунсинга ввода в компоненте Autocomplete
от Material UI в сочетании с библиотекой react-hook-form
, необходимо carefully управлять состояниями компонента и запросами к серверу. Давайте разберёмся, как это правильно сделать, чтобы избежать проблемы, когда состояние ввода и отображаемые варианты не соответствуют друг другу.
Проблема
Основная проблема заключается в том, что когда пользователь вводит имя клиента, происходят обновления состояния, что вызывает повторный рендер компонента. В момент, когда происходит обновление, значение в контроле react-hook-form
может не совпадать с текущим вводом. Изражевая логика приводит к тому, что eldere значение отрисовывается в поле вместо того, чтобы видеть введённые данные пользователем.
Решение
Для того чтобы избежать проблемы и сделать обработку ввода более плавной и отзывчивой, вы можете применить следующую технику:
-
Создание локального состояния для ввода: Создайте отдельное состояние для хранения значения ввода и используйте его для обработки имён клиентов. Это позволит вам управлять вводом независимо от состояния контроллера react-hook-form.
-
Использование debounce: Сделайте так, чтобы изменение состояний происходило с задержкой, используя такие библиотеки, как
lodash
для реализации функции дебаунса. -
Синхронизация состояния: Когда пользователь завершает ввод, отправляйте это значение в функцию, обновляющую список опций.
Пример кода
Вот пример того, как можно реализовать вышеописанное решение:
import React, { useState, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { Autocomplete } from '@mui/material';
import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import debounce from 'lodash.debounce';
import { useQuery } from 'react-query'; // Предполагается, что используются react-query
export function FormAutocompleteField({
control,
name,
loading,
label,
onInputChange,
debounceTime = 300,
}) {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (event, value) => {
setInputValue(value);
onInputChange(value);
};
return (
<Controller
control={control}
name={name}
render={({ field, fieldState: { error } }) => (
<Autocomplete
options={[]} // Здесь нужно задать ваши варианты
inputValue={inputValue}
onInputChange={debounce(handleInputChange, debounceTime)}
onChange={(_, value) => field.onChange(value?.id)}
renderInput={(params) => (
<TextField
{...params}
label={label}
error={!!error?.message}
helperText={error?.message}
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
)}
/>
);
}
export default function useJobOfferForm() {
const [customerName, setCustomerName] = useState('');
const form = useForm();
// ваша логика запроса для получения клиентов
const customersQuery = useQuery(/* ваш запрос */);
const onCustomerNameChange = (name) => {
setCustomerName(name);
};
useEffect(() => {
// Здесь можете отслеживать изменение customerName
}, [customerName]);
return {
form,
onCustomerNameChange,
};
}
// Ваша основная форма
export default function JobOfferForm() {
const { form, onCustomerNameChange } = useJobOfferForm();
return (
<form onSubmit={form.handleSubmit(/* ваш обработчик */)}>
<FormAutocompleteField
control={form.control}
name="customerId"
label="Выберите клиента"
loading={/* Загрузка данных */}
onInputChange={onCustomerNameChange}
/>
{/* Прочие элементы формы */}
</form>
);
}
Заключение
Использование дебаунса вместе с управлением локальным состоянием ввода сделает ваш компонент Autocomplete
более отзывчивым и избежать проблем с синхронизацией значений. Как следствие, пользователь будет видеть актуальное состояние ввода, а ваши запросы к серверу будут оптимально организованы. Такой подход повысит общий пользовательский опыт и уменьшит нагрузку на базу данных за счёт уменьшения количества запросов.