← Назад к вопросам

Как реализовать автокомплит с запросом на сервер?

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

Ключевые оптимизации

  1. Дебаунс - ждать 300-500ms перед запросом
  2. AbortController - отменять старые запросы
  3. Минимальная длина - запрос только при 3+ символа
  4. Кэширование - помнить уже загруженные результаты
  5. Limit - ограничивать количество результатов (10-20)

Автокомплит требует внимания к производительности - это один из самых частых UX элементов.