← Назад к вопросам
Как реализовать автокомплит с запросом на сервер?
2.0 Middle🔥 121 комментариев
#JavaScript Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Реализация автокомплита с запросом на сервер
Автокомплит требует оптимизации - нельзя делать запрос на каждый символ. Нужны дебаунс, отмена старых запросов и правильное отображение результатов.
Базовая реализация с дебаунсом
import { useState, useRef, useEffect } from 'react';
export function Autocomplete() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debounceTimerRef = useRef(null);
useEffect(() => {
// Очищаем предыдущий таймер
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// Если поле пусто, очищаем результаты
if (!query) {
setResults([]);
return;
}
// Ждем 300ms до нового запроса
debounceTimerRef.current = setTimeout(() => {
searchItems(query);
}, 300);
return () => clearTimeout(debounceTimerRef.current);
}, [query]);
const searchItems = async (searchQuery) => {
setLoading(true);
try {
const response = await fetch(
`/api/v1/items/search?q=${encodeURIComponent(searchQuery)}`
);
const data = await response.json();
setResults(data.items);
} catch (error) {
console.error('Ошибка поиска:', error);
setResults([]);
} finally {
setLoading(false);
}
};
return (
<div className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск..."
className="w-full border rounded px-3 py-2"
/>
{loading && <div className="absolute top-full mt-1 text-gray-500">Загрузка...</div>}
{results.length > 0 && (
<ul className="absolute top-full left-0 right-0 bg-white border rounded shadow-lg mt-1">
{results.map((item) => (
<li
key={item.id}
className="px-3 py-2 hover:bg-gray-100 cursor-pointer"
onClick={() => setQuery(item.name)}
>
{item.name}
</li>
))}
</ul>
)}
</div>
);
}
Отмена предыдущего запроса (AbortController)
import { useState, useRef, useEffect } from 'react';
export function SmartAutocomplete() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const abortControllerRef = useRef(null);
const debounceTimerRef = useRef(null);
useEffect(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (!query) {
setResults([]);
return;
}
debounceTimerRef.current = setTimeout(() => {
// Отменяем предыдущий запрос
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Создаем новый AbortController
abortControllerRef.current = new AbortController();
searchItems(query, abortControllerRef.current.signal);
}, 300);
return () => {
clearTimeout(debounceTimerRef.current);
abortControllerRef.current?.abort();
};
}, [query]);
const searchItems = async (searchQuery, signal) => {
setLoading(true);
try {
const response = await fetch(
`/api/v1/items/search?q=${encodeURIComponent(searchQuery)}`,
{ signal }
);
const data = await response.json();
setResults(data.items);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Ошибка поиска:', error);
}
setResults([]);
} finally {
setLoading(false);
}
};
return (
<div className="relative">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск..."
/>
{loading && <div>Поиск...</div>}
{results.length > 0 && (
<ul>
{results.map((item) => (
<li key={item.id} onClick={() => setQuery(item.name)}>
{item.name}
</li>
))}
</ul>
)}
</div>
);
}
С использованием useCallback и useReducer
import { useReducer, useCallback, useRef, useEffect } from 'react';
const initialState = {
query: '',
results: [],
loading: false,
error: null
};
function reducer(state, action) {
switch (action.type) {
case 'SET_QUERY':
return { ...state, query: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_RESULTS':
return { ...state, results: action.payload, error: null };
case 'SET_ERROR':
return { ...state, error: action.payload, results: [] };
default:
return state;
}
}
export function AdvancedAutocomplete() {
const [state, dispatch] = useReducer(reducer, initialState);
const abortControllerRef = useRef(null);
const debounceTimerRef = useRef(null);
const performSearch = useCallback(async (searchQuery, signal) => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const response = await fetch(
`/api/v1/items/search?q=${encodeURIComponent(searchQuery)}`,
{ signal }
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
dispatch({ type: 'SET_RESULTS', payload: data.items });
} catch (error) {
if (error.name !== 'AbortError') {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, []);
const handleQueryChange = useCallback((newQuery) => {
dispatch({ type: 'SET_QUERY', payload: newQuery });
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (!newQuery) {
dispatch({ type: 'SET_RESULTS', payload: [] });
return;
}
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
debounceTimerRef.current = setTimeout(() => {
performSearch(newQuery, abortControllerRef.current.signal);
}, 300);
}, [performSearch]);
useEffect(() => {
return () => {
clearTimeout(debounceTimerRef.current);
abortControllerRef.current?.abort();
};
}, []);
return (
<div>
<input
value={state.query}
onChange={(e) => handleQueryChange(e.target.value)}
placeholder="Поиск..."
/>
{state.error && <div className="text-red-500">{state.error}</div>}
{state.loading && <div className="text-gray-500">Загрузка...</div>}
{state.results.length > 0 && (
<ul>
{state.results.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}
С RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { useState, useCallback } from 'react';
const searchApi = createApi({
reducerPath: 'searchApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api/v1' }),
endpoints: (builder) => ({
searchItems: builder.query({
query: (searchQuery) => `/items/search?q=${encodeURIComponent(searchQuery)}`,
// Пропускаем запрос если нет поискового текста
queryFn: async (searchQuery, api) => {
if (!searchQuery) {
return { data: { items: [] } };
}
return api.baseQuery(
`/items/search?q=${encodeURIComponent(searchQuery)}`
);
}
})
})
});
export function RtkAutocomplete() {
const [query, setQuery] = useState('');
const { data, isFetching } = searchApi.useSearchItemsQuery(
query,
{ skip: !query }
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Поиск..."
/>
{isFetching && <div>Загрузка...</div>}
{data?.items && (
<ul>
{data.items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</div>
);
}
Ключевые оптимизации
- Дебаунс - ждать 300-500ms перед запросом
- AbortController - отменять старые запросы
- Минимальная длина - запрос только при 3+ символа
- Кэширование - помнить уже загруженные результаты
- Limit - ограничивать количество результатов (10-20)
Автокомплит требует внимания к производительности - это один из самых частых UX элементов.