Skip to content

withDebounce ​

Метод withDebounce позволяет создавать дебаунс-функции, которые откладывают выполнение асинхронных операций на заданное время. Это полезно для оптимизации производительности, например, при обработке событий ввода пользователя.

Синтаксис ​

javascript
withDebounce(fetcher, options)

Параметры ​

  • fetcher (Function): Асинхронная функция для загрузки данных.

    • Принимает два аргумента: source и signal.
    • Возвращает промис с данными типа T.
  • options (DebounceOptions): Опции дебаунса.

Возвращаемое значение ​

Метод возвращает обернутую функцию, которая принимает source и signal, и возвращает промис с данными типа T.

Примеры ​

Пример на чистом JavaScript ​

javascript
const engine = new ReactiveEngine();

const fetchData = async (query, abortSignal) => {
  const res = await fetch(
    `https://api.example.com/data?query=${encodeURIComponent(query)}`,
    { signal: abortSignal }
  );
  if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
  return res.json();
};

const debouncedFetch = withDebounce(fetchData, { delay: 400 });

const querySignal = engine.signal('', 'search:signal:query');

querySignal.subscribe((query) => {
  debouncedFetch(query, new AbortController().signal).then(data => {
    console.log('Fetched data:', data);
  }).catch(error => {
    console.error('Error fetching data:', error);
  });
});

// Обновляем querySignal при вводе пользователя

Пример с интеграцией в resource ​

javascript
const engine = new ReactiveEngine();

const fetchData = async (query, abortSignal) => {
  const res = await fetch(
    `https://api.example.com/data?query=${encodeURIComponent(query)}`,
    { signal: abortSignal }
  );
  if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
  return res.json();
};

const debouncedFetch = withDebounce(fetchData, { delay: 400 });

const querySignal = engine.signal('', 'search:signal:query');
const searchResource = engine.resource(debouncedFetch, querySignal, 'my-resource');

searchResource.subscribe((state) => {
  console.log('Resource state:', state);
});

// Обновляем querySignal при вводе пользователя

Дополнительная информация ​

Декоратор withDebounce полезен для оптимизации запросов к серверу, особенно при обработке событий ввода пользователя. Он позволяет откладывать выполнение запросов до тех пор, пока пользователь не перестанет вводить текст, что уменьшает количество ненужных запросов и улучшает производительность.

Пример использования ​

ts
import { AbstractService } from '@pravosleva/reactive-engine'

interface DebounceOptions {
  delay?: number
}

export const withDebounce = <S, T>(
  fetcher: (source: S, signal: AbortSignal) => Promise<T>,
  options: DebounceOptions = {}
) => {
  const delay = options.delay ?? 300
  let timeoutId: ReturnType<typeof setTimeout> | null = null
  let rejectPrevious: ((reason: any) => void) | null = null

  return (source: S, signal: AbortSignal): Promise<T> => {
    if (timeoutId) clearTimeout(timeoutId)
    if (rejectPrevious) {
      rejectPrevious(new DOMException('Aborted due to debounce', 'AbortError'))
    }

    return new Promise<T>((resolve, reject) => {
      rejectPrevious = reject

      const onAbort = () => {
        if (timeoutId) clearTimeout(timeoutId)
        reject(new DOMException('Aborted by resource signal', 'AbortError'))
      }

      if (signal.aborted) return onAbort()
      signal.addEventListener('abort', onAbort)

      timeoutId = setTimeout(async () => {
        signal.removeEventListener('abort', onAbort)
        rejectPrevious = null
        timeoutId = null

        try {
          const data = await fetcher(source, signal)
          resolve(data)
        } catch (error) {
          reject(error)
        }
      }, delay)
    })
  }
}

// Сам бизнес-сервис
export class SearchLogic extends AbstractService {
  // Сигнал, куда React-инпут будет записывать текст на каждый символ
  public querySignal = this.createSignal<string>('', 'search:signal:query')

  /**
   * Реактивный ресурс, обёрнутый в декоратор withDebounce.
   * Движок автоматически перезапускает его при изменении querySignal,
   * но декоратор принудительно задерживает реальное выполнение на 500 мс.
   */
  public searchResource = this.engine.resource(
    withDebounce(
      async (queryValue, _abortSignal) => {
        // Имитируем задержку ответа от сервера (например, чтение из базы)
        await new Promise((resolve) => setTimeout(resolve, 400))

        // Фейковый результат поиска
        // В этом месте возвращается массив строк исключительно ради наглядности демонстрации в UI
        // (чтобы в блоке результатов под инпутом можно было отрендерить список с помощью метода .map()).
        return [
          `Результат 1 для "${queryValue}"`,
          `Результат 2 для "${queryValue}"`,
          `Результат 3 для "${queryValue}"`
        ]
      },
      { delay: 500 } // Задержка дебаунса 500 мс
    ),
    this.querySignal,
    {
      name: 'search:resource:fetch',
      // Не отправляем запрос, если инпут пустой
      validateBeforeFetch: (queryValue) => !!queryValue.trim()
    }
  )

  /**
   * Экшен обновления поисковой строки из UI
   */
  public updateQuery(val: string) {
    this.querySignal.value = val
  }
}
tsx
import { ReactiveEngine, useReactiveValue } from '@pravosleva/reactive-engine/react'
import { SearchLogic } from './service.SearchLogic'
import { Input } from '~/shared/Input'
import baseClasses from '~/ui.common.module.scss'
import clsx from 'clsx'

const engine = new ReactiveEngine()

export const SearchExample = () => {
  const logic = engine.inject(SearchLogic)

  // Подписываемся на сигналы и ресурс
  const query = engine.use(logic.querySignal)
  const { loading, data: results, error } = useReactiveValue(logic.searchResource)

  return (
    <div
      className={clsx(baseClasses.unit, baseClasses.stack2)}
      style={{
        fontFamily: 'system-ui',
        width: 'max(100px, calc(100vw - 24px - 24px - 24px - 24px - 16px - 16px - 4px - 4px))'
      }}
    >
      <div className={baseClasses.absoluteUnitLabel}>Simple Debounce Search Demo</div>

      {/* Поле ввода текста */}
      <div className={baseClasses.stack1} style={{ width: '100%', color: '#000' }}>
        <label style={{ fontSize: 'small' }}>Живой поиск (дебаунс 500мс):</label>
        <Input
          variant='outlined'
          type="text"
          placeholder="Начните вводить текст..."
          value={query}
          onChange={(e) => logic.updateQuery(e.target.value)}
        />
      </div>

      {/* Статус-бар загрузки */}
      <div className={baseClasses.stack1} style={{ fontSize: 'small' }}>
        {
          loading
            ? <span style={{ color: '#e6af2e' }}>⏳ Ждем окончания ввода и ответа сервера...</span>
            : (query && !results)
              ? <span>Печатайте дальше...</span>
              : <span>Печатайте дальше...</span>
        }
        {error && <span style={{ color: '#ef5350' }}>❌ Ошибка: {error.message}</span>}
      </div>

      {/* Отрендеренный список результатов */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', width: '100%' }}>
        <div style={{ fontSize: 'small' }}>Результаты выдачи:</div>
        <div style={{ background: '#111', borderRadius: '6px', padding: '12px', minHeight: '80px', display: 'flex', flexDirection: 'column', gap: '6px', fontSize: '13px' }}>
          {results && results.map((item, idx) => (
            <div key={idx} style={{ color: '#4caf50' }}>{item}</div>
          ))}
          {!query.trim() && <span style={{ color: '#aaa' }}>Строка поиска пуста</span>}
          {query.trim() && !loading && !results && <span style={{ color: '#aaa' }}>Запрос задебаунсен...</span>}
        </div>
      </div>
    </div>
  )
}