You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

useSearchParams引发useEffect无限循环,求复刻anilist.co搜索功能方案

问题分析与解决方案

为什么会出现无限循环?

useSearchParams 返回的 URLSearchParams 对象每次组件渲染都会生成新的引用,哪怕地址栏的查询参数完全没变化。如果把整个 qp 对象放进 useEffect 的依赖数组,组件每次渲染都会触发 useEffect,发起API请求后组件重新渲染,又生成新的 qp 对象,最终导致无限循环。

快速修复方案

不要依赖整个 qp 对象,而是提取具体的查询参数值作为依赖:

import { useSearchParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

const SearchPage = () => {
  const [qp] = useSearchParams();
  // 提取具体的搜索参数值,而非整个qp对象
  const searchQuery = qp.get('q');
  const [results, setResults] = useState([]);

  useEffect(() => {
    // 空查询时跳过请求
    if (!searchQuery?.trim()) {
      setResults([]);
      return;
    }

    // 发起Anilist API请求
    const fetchAnime = async () => {
      try {
        const res = await fetch('https://api.anilist.co/graphql', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            query: `
              query ($search: String) {
                Page(page: 1, perPage: 20) {
                  media(search: $search, type: ANIME) {
                    id
                    title { romaji english }
                    coverImage { large }
                  }
                }
              }
            `,
            variables: { search: searchQuery }
          })
        });
        const data = await res.json();
        setResults(data.data.Page.media);
      } catch (err) {
        console.error('搜索失败:', err);
      }
    };

    fetchAnime();
  }, [searchQuery]); // 仅当searchQuery实际变化时触发

  // 渲染搜索框和结果...
};

更优实现方案

针对Anilist搜索场景,推荐封装成可复用的自定义Hook,同时加入防抖、状态管理等优化:

1. 封装自定义Hook useAnimeSearch

import { useSearchParams, useNavigate } from 'react-router-dom';
import { useEffect, useState, useCallback } from 'react';

// 简易防抖实现(也可使用lodash.debounce)
const debounce = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

const useAnimeSearch = () => {
  const [qp] = useSearchParams();
  const navigate = useNavigate();
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const currentQuery = qp.get('q');

  // 防抖后的请求函数,避免输入过程中频繁发起请求
  const fetchSearchResults = useCallback(
    debounce(async (query) => {
      if (!query?.trim()) {
        setResults([]);
        return;
      }
      setLoading(true);
      setError(null);

      try {
        const res = await fetch('https://api.anilist.co/graphql', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            query: `
              query ($search: String) {
                Page(page: 1, perPage: 20) {
                  media(search: $search, type: ANIME) {
                    id
                    title { romaji english }
                    coverImage { large }
                    startDate { year }
                  }
                }
              }
            `,
            variables: { search: query }
          })
        });

        if (!res.ok) throw new Error('请求失败');
        const data = await res.json();
        setResults(data.data.Page.media);
      } catch (err) {
        setError('搜索失败,请稍后重试');
      } finally {
        setLoading(false);
      }
    }, 300), // 300ms防抖延迟
    []
  );

  // 监听查询参数变化
  useEffect(() => {
    fetchSearchResults(currentQuery || '');
    // 组件卸载时清理防抖定时器
    return () => fetchSearchResults.cancel();
  }, [currentQuery, fetchSearchResults]);

  // 更新地址栏查询参数的方法(输入框联动地址栏)
  const updateSearchQuery = (query) => {
    const newParams = new URLSearchParams(qp);
    if (query.trim()) {
      newParams.set('q', query);
    } else {
      newParams.delete('q');
    }
    // 使用replace避免历史记录堆积
    navigate(`?${newParams.toString()}`, { replace: true });
  };

  return {
    results,
    loading,
    error,
    currentQuery,
    updateSearchQuery
  };
};

2. 使用自定义Hook构建搜索页面

const SearchPage = () => {
  const { results, loading, error, currentQuery, updateSearchQuery } = useAnimeSearch();

  return (
    <div className="search-container">
      <div className="search-bar">
        <input
          type="text"
          value={currentQuery || ''}
          onChange={(e) => updateSearchQuery(e.target.value)}
          placeholder="搜索动画..."
          className="search-input"
        />
      </div>

      {loading && <div className="loading">加载中...</div>}
      {error && <div className="error-message">{error}</div>}

      <div className="results-grid">
        {results.map((anime) => (
          <div key={anime.id} className="anime-card">
            <img
              src={anime.coverImage.large}
              alt={anime.title.english || anime.title.romaji}
              className="anime-cover"
            />
            <div className="anime-info">
              <h3>{anime.title.english || anime.title.romaji}</h3>
              {anime.startDate.year && <p>{anime.startDate.year}</p>}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

优化点说明

  • 防抖处理:用户输入过程中仅在停止输入300ms后发起请求,减少无效API调用
  • 状态管理:统一处理加载、错误、结果状态,提升用户体验
  • 路由同步:输入框内容与地址栏参数双向同步,支持页面刷新后保留搜索状态
  • 复用性:自定义Hook可在多个页面复用,减少重复代码

内容的提问来源于stack exchange,提问作者ayushwashere

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 23:45:10