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

React项目中Spinner组件实现异常问题及优化需求

React Spinner组件优化:按需触发加载状态

问题根源分析

原实现存在两个核心问题:

  • 加载状态本地化:SearchResults组件内部维护isDataLoading,仅在组件挂载时通过setTimeout切换状态,后续数据更新(如点击Employees按钮)无法触发Spinner。
  • 同步数据无感知延迟:本地数据过滤速度极快,即使触发加载状态,Spinner也会因状态切换过快而无法被用户看到。

优化方案

  1. 状态提升:将isDataLoading移至父组件,统一控制所有操作的加载状态。
  2. 操作触发加载:在页面初始化、点击Employees按钮、搜索操作开始时开启加载状态,数据处理完成后(模拟合理延迟)关闭状态。
  3. 按需显示Spinner:搜索操作根据输入长度动态调整延迟,短输入快速过滤可跳过延迟,长输入或复杂过滤则显示Spinner。

修改后代码实现

父组件(EmployeeSearch.js)

统一管理数据与加载状态,处理所有数据操作逻辑:

import { useState, useEffect } from 'react';
import SearchText from './SearchText';
import SearchResults from './SearchResults';

// 测试数据
const data = [{"ROW_NUMBER": 1,"PIDM": 991,"NAME": "Elvis Presley","PHONE": "999-999-9999","EMAIL": "<a href=mailto:nothing-to-see-here@nowhere.edu>test</a>","CAMPUS": "Main 1","DEPARTMENT": "Eng /Read /Frgn Lang/ IDS / Comm.","JOBTITLE": null,"SEARCHTYPE": null,"SEARCHHEADER": null}, {"ROW_NUMBER": 2,"PIDM": 992,"NAME": "James Dean","PHONE": "999-999-9999","EMAIL": "<a href=mailto:nothing-to-see-here@nowhere.edu>test</a>","CAMPUS": "Main 2","DEPARTMENT": "Instructional Resources","JOBTITLE": "Lead Librarian-Systems","SEARCHTYPE": null,"SEARCHHEADER": null}, {"ROW_NUMBER": 3,"PIDM": 993,"NAME": "Marilyn Monroe","PHONE": "999-999-9999","EMAIL": "<a href=mailto:nothing-to-see-here@nowhere.edu>test</a>","CAMPUS": "Main 3","DEPARTMENT": "Housekeeping","JOBTITLE": "Custodian","SEARCHTYPE": null,"SEARCHHEADER": null}];

const EmployeeSearch = () => {
  const [searchResults, setSearchResults] = useState([]);
  const [isDataLoading, setIsDataLoading] = useState(true);

  // 页面初始化加载全量数据
  useEffect(() => {
    loadFullData();
  }, []);

  // 加载全量数据(含延迟模拟)
  const loadFullData = () => {
    setIsDataLoading(true);
    setTimeout(() => {
      setSearchResults(data);
      setIsDataLoading(false);
    }, 300);
  };

  // 搜索过滤逻辑
  const handleSearch = (query) => {
    setIsDataLoading(true);
    const fields = ['NAME', 'CAMPUS', 'DEPARTMENT', 'JOBTITLE'];
    
    // 根据输入长度动态调整延迟:短输入快速过滤无延迟
    const delay = query.length > 2 ? 200 : 0;

    setTimeout(() => {
      const filteredData = data.filter(record => 
        fields.some(field => record[field]?.toLowerCase().includes(query.toLowerCase()))
      );
      setSearchResults(filteredData);
      setIsDataLoading(false);
    }, delay);
  };

  // 重置搜索
  const handleReset = () => {
    loadFullData();
  };

  return (
    <div>
      <SearchText 
        onSearch={handleSearch}
        onLoadEmployees={loadFullData}
        onReset={handleReset}
      />
      <SearchResults 
        searchResults={searchResults}
        isDataLoading={isDataLoading}
      />
    </div>
  );
};

export default EmployeeSearch;

修改后的SearchText组件

仅负责UI交互,触发父组件的操作事件:

import { useState } from 'react';
import { faMagnifyingGlass, faPeopleGroup, faBuildingUser, faArrowsRotate } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import utilStyles from '../styles/utils.module.css';

const SearchText = ({ onSearch, onLoadEmployees, onReset }) => {
  const [enteredText, setEnteredText] = useState("");

  const onFormSubmit = (event) => {
    event.preventDefault();
  };
  
  const onChange = (event) => {
    const query = event.target.value;
    setEnteredText(query);
    onSearch(query);
  };

  const onEmployees = () => {
    setEnteredText("");
    onLoadEmployees();
  };

  const handleReset = () => {
    setEnteredText("");
    onReset();
  };

  return (
    <>
      <form id='EmployeeSearchText' onSubmit={onFormSubmit}>
        <div className="container-fluid p-3">
          <div id='Row1' className={`row ${utilStyles.divPadding}`}>
            <div id='Row1Column1' className="col">
              <div id='SearchText-container' className={utilStyles.searchBar}>
                <FontAwesomeIcon icon={faMagnifyingGlass} className={utilStyles.icon} />
                <input 
                  id='SearchText' 
                  type="text" 
                  name="SearchText" 
                  placeholder="Search Last Name; First Name; Campus; Department....." 
                  value={enteredText} 
                  onChange={onChange} 
                  className={`form-control ${utilStyles.inputField} ${utilStyles.inputPadding}`} 
                />
              </div>
            </div>
          </div>
          <div className='text-center'>
            <div id='Row2' className={`row ${utilStyles.divPadding}`}>
              <div id='Row2Column1' className="col">
                <div id='Employees-container'>
                  <button 
                    id='EmployeeListing' 
                    type="button" 
                    onClick={onEmployees} 
                    className={utilStyles.btnYellow}
                  >
                    Employees&nbsp;&nbsp;<FontAwesomeIcon icon={faPeopleGroup} />
                  </button>
                </div>
              </div>
              <div id='Row2Column2' className="col">
                <div id='DepartmentListing-container'>
                  <button id='DepartmentListing' type="button" className={utilStyles.btnYellow}>
                    Department Listing&nbsp;&nbsp;<FontAwesomeIcon icon={faBuildingUser} />
                  </button>
                </div>
              </div>
              <div id='Row2Column3' className="col">
                <div id='SearchTextReset-container'>
                  <button 
                    id='SearchTextReset' 
                    type="button" 
                    onClick={handleReset} 
                    className={utilStyles.btnGrey}
                  >
                    Reset&nbsp;&nbsp;<FontAwesomeIcon icon={faArrowsRotate} />
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </form>     
    </>
  );
};

export default SearchText;

修改后的SearchResults组件

根据父组件传入的状态渲染内容:

import { RotatingLines } from "react-loader-spinner";
import parse from 'html-react-parser';
import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import utilStyles from '../styles/utils.module.css';

const SearchResults = ({ searchResults, isDataLoading }) => {

  const Loader = () => {
    return (
      <RotatingLines
        strokeColor="#51abff"
        strokeWidth="5"
        animationDuration="0.75"
        width="100"
        visible={true}
      />
    );
  };

  const emp = searchResults.map(employee =>
    <div className="col col-12 col-sm-6 col-xl-4 mb-4" key={employee.PIDM}>
      <div className="col-12 mb-4">
        <div className={`card ${utilStyles.cardBody}`}>
          <div className="card-body">
            <div className={`${utilStyles.ribbonLight} ${utilStyles.ribbonBottomRight}`}><span>{employee.CAMPUS}</span></div>
            <h3 className="card-title card-name">{employee.NAME}</h3>
            <h6 className={`${utilStyles.cardJobtitle} mb-2`}>{employee.JOBTITLE || '-'}</h6>
            <p className={`${utilStyles.cardText} ${utilStyles.cardDepartment}`}>{employee.DEPARTMENT}</p>
            <div>
              <p className={utilStyles.cardTextBottom}>
                <FontAwesomeIcon icon={faPhone} className={utilStyles.cardTextIcons} />
                <span className={utilStyles.cardPhoneEmail}>W:</span>{employee.PHONE}
              </p>
              <p className={utilStyles.cardTextBottom}>
                <FontAwesomeIcon icon={faEnvelope} className={utilStyles.cardTextIcons} />
                <span className={utilStyles.cardPhoneEmail}>W:</span>{parse(employee.EMAIL)}
              </p>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
  
  const content = emp.length ? emp : <article><p className={`${utilStyles.cardResultCount}`}>No Matching Results</p></article>;

  return (
    <div className="container">
      <div className="row">
        {isDataLoading ? (
          <div style={{width: "100px", margin: "auto"}}><Loader /></div>
        ) : content}
      </div>
    </div>
  );
};

export default SearchResults;

关键优化点

  • 状态统一管理:父组件控制所有操作的加载状态,确保页面初始化、点击Employees按钮、搜索时都能触发Spinner。
  • 动态延迟控制:根据搜索输入长度调整延迟,平衡用户体验与性能。
  • 组件解耦:UI组件与数据逻辑分离,代码更易维护。
  • React最佳实践:为列表元素添加唯一key属性,避免渲染警告。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 19:05:54