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>
)
}