Дебаунсинг ввода Mui Autocomplete с использованием react-hook-form

Вопрос или проблема

Я использую 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 значение отрисовывается в поле вместо того, чтобы видеть введённые данные пользователем.

Решение

Для того чтобы избежать проблемы и сделать обработку ввода более плавной и отзывчивой, вы можете применить следующую технику:

  1. Создание локального состояния для ввода: Создайте отдельное состояние для хранения значения ввода и используйте его для обработки имён клиентов. Это позволит вам управлять вводом независимо от состояния контроллера react-hook-form.

  2. Использование debounce: Сделайте так, чтобы изменение состояний происходило с задержкой, используя такие библиотеки, как lodash для реализации функции дебаунса.

  3. Синхронизация состояния: Когда пользователь завершает ввод, отправляйте это значение в функцию, обновляющую список опций.

Пример кода

Вот пример того, как можно реализовать вышеописанное решение:

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 более отзывчивым и избежать проблем с синхронизацией значений. Как следствие, пользователь будет видеть актуальное состояние ввода, а ваши запросы к серверу будут оптимально организованы. Такой подход повысит общий пользовательский опыт и уменьшит нагрузку на базу данных за счёт уменьшения количества запросов.

Оцените материал
Добавить комментарий

Капча загружается...