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

React Hooks+Axios搜索输入框失焦、重渲染及组件抽象问题咨询

问题修复与优化方案

输入框输入触发全页重渲染、丢失焦点的根因与修复

核心诱因是你将搜索组件SeacrhComponent(存在拼写错误,Search少写了r)直接定义在FsacList组件函数内部。

  • 每次FsacList因state更新触发重渲染时,嵌套在内部的SeacrhComponent都会被重新创建为全新的函数引用,React在虚拟DOM diff阶段会判定这是完全不同的组件类型,直接卸载旧的输入框DOM、挂载新的输入框DOM,焦点自然会丢失。
  • 除此之外代码里还有几处会触发不必要重渲染、重复请求的问题:
    • BASE_URL在每次组件渲染时都会生成新的字符串引用,被放在useEffect依赖数组中会触发无意义的effect重执行
    • 搜索框每输入一个字符就会立即更新params触发接口请求,会产生大量无效的中间请求
    • 三个筛选排序的点击处理函数每次渲染都是新引用,后续做子组件渲染优化时会失效

对应的修复方式:

  1. 将搜索组件挪到FsacList组件外部定义,或者直接将input元素写在组件return的对应位置,避免嵌套定义组件导致的引用不稳定;如果需要抽离组件,配合React.memo做渲染缓存即可。
  2. 将固定接口前缀提到组件外部,动态拼接的baseUrl使用useState或useMemo缓存,仅当props.fsac变化时重新计算。
  3. 给搜索输入加300-500ms的防抖,等用户停止输入后再更新请求用的search参数,减少无效请求。
  4. 事件处理函数可以用useCallback缓存引用,配合子组件memo减少不必要重渲染。

FsacList组件的更优实现方案

抽象通用组件承载四类数据渲染的思路是可行的,除了全量props传值的方式外,有几个复用性、灵活性更强的实现方向:

  • 抽离自定义Hook:把四类列表通用的参数状态、数据请求、筛选/排序/分页逻辑全部抽为独立的useFsacList Hook,使用时仅需传入fsac类型参数,Hook直接返回loading状态、列表数据、绑定好的操作方法,不同业务页面可以根据需求自由组装UI,不会被固定的组件结构限制,复用性远高于封装成单体大组件。
  • 组合式组件模式:不要把所有UI结构、配置都写死在FsacList内部,将组件拆分为根容器、搜索框、类型筛选、年份筛选、排序栏、列表内容等多个子组件,通过React Context在组件树内部传递状态和操作方法,使用时可以按需调整组件顺序、自定义局部样式,不需要通过props层层传递几十个配置项。
  • 全局状态托管:如果四类列表的筛选状态、数据需要在多个路由、跨层级组件中共享,可以将请求逻辑、状态操作统一放到Zustand/Redux/Jotai等全局状态库中,组件本身只负责消费状态渲染UI,跨组件使用时不需要层层传参。

修复后核心代码参考

// 把常量、子组件提到组件外部,保证引用稳定
const BASE_API_PREFIX = 'http://127.0.0.1:8000/api/';

// 抽离独立的搜索组件,配合memo做缓存
const SearchInput = React.memo(({ value, onChange }) => (
  <input
    type="text"
    placeholder="Search"
    value={value}
    onChange={e => onChange(e.target.value)}
  />
));

// 简单防抖工具
const debounce = (func, delay = 400) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  }
};

const FsacList = (props) => {
  const [baseUrl, setBaseUrl] = useState(() => `${BASE_API_PREFIX}${props.fsac}`);
  const now = new Date().getUTCFullYear();
  const years = useMemo(() => Array(now - (now - 73)).fill('').map((v, idx) => now - idx), [now]);

  const [loading, setLoading] = useState(true);
  const [data, setData] = useState([]);
  const [genres, setGenres] = useState([]);
  const [params, setParams] = useState({
    page: '1',
    ordering: '-created',
    search: '',
    year: '',
    genre__name: '',
  });

  // 缓存搜索更新方法,加防抖
  const updateSearch = useCallback(debounce((val) => {
    setParams(prev => ({...prev, search: val}));
  }), []);

  // 缓存事件处理函数
  const genreClick = useCallback((e, genre) => {
    setParams(prev => ({
      ...prev,
      genre__name: prev.genre__name === genre ? '' : genre
    }));
  }, []);

  const yearClick = useCallback((e, year) => {
    setParams(prev => ({
      ...prev,
      year: prev.year === year ? '' : year
    }));
  }, []);

  const sortClick = useCallback((e, field) => {
    setParams(prev => {
      if (prev.ordering === field) return {...prev, ordering: `-${field}`};
      return {...prev, ordering: field};
    });
  }, []);

  // 同步fsac变化更新baseUrl
  useEffect(() => {
    setBaseUrl(`${BASE_API_PREFIX}${props.fsac}`);
  }, [props.fsac]);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const [listRes, genreRes] = await axios.all([
          axios.get(`${baseUrl}/`, {params}),
          axios.get(`${baseUrl}-genres/`)
        ]);
        setData(listRes.data);
        setGenres(genreRes.data);
      } catch (error) {
        console.error(error.message);
      }
      setLoading(false);
    }
    fetchData();
  }, [baseUrl, params]);

  return (
    <>
      {loading && <Spinner/>}
      {!loading && (
        <div>
          <SearchInput value={params.search} onChange={updateSearch}/>
          <ul>
            {genres.map(genre =>
              <li key={genre.id}>
                <button onClick={(e) => genreClick(e, genre.name)}>
                  {genre.name}</button>
              </li>
            )}
          </ul>
          <ul>
            {years.map(year =>
              <li key={year}>
                <button onClick={(e) => yearClick(e, year)}>
                  {year}</button>
              </li>
            )}
          </ul>
          <ul>
            <li>
              <button onClick={(e) => sortClick(e, 'title')}>Title</button>
              <button onClick={(e) => sortClick(e, 'year')}>Year</button>
              <button onClick={(e) => sortClick(e, 'rating')}>Rating</button>
              <button onClick={(e) => sortClick(e, 'created')}>Date</button>
            </li>
          </ul>
          <ul>
            {data.results.map(item =>
              <li key={item.id}>
                <p>{item.title}</p>
                <p>{item.year}</p>
                <p>{item.desc}</p>
                <p>{item.rating}</p>
              </li>
            )}
          </ul>
        </div>
      )}
    </>
  )
}

export default FsacList;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:30:56