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

SyncFusion自定义绑定Grid加载Spinner显示异常求助

SyncFusion Grid 加载Spinner异常问题(React 18 + Redux)

问题现象

  • 初始请求数据填充Grid时,Redux的isLoading状态已正常触发,控制台日志显示副作用执行无误,但加载Spinner始终不显示。
  • 初始数据返回并填充Grid后,Spinner突然出现且无法自动消失;移除行详情模板(detail template)后该现象消失;点击外部列选择器按钮调用hideSpinner()后,后续分页、排序请求的Spinner显示恢复正常。

相关代码

Redux Slice 代码

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { DataStateChangeEventArgs } from "@syncfusion/ej2-react-grids";
import { ServiceRequest } from "./models/ServiceRequest.interface";
import { ServiceRequestResult } from "./models/ServiceRequestResult.interface";
import csmService from "./services/csmMyRequestService";

interface AsyncState {
  isLoading: boolean;
  isSuccess: boolean;
  isError: boolean;
}

interface MyRequestState extends AsyncState {
  result: ServiceRequest[];
  count: number;
}

const initialState: MyRequestState = {
  isLoading: false,
  isSuccess: false,
  isError: false,
  result:[], 
  count: 0
}

export const getMyRequests = createAsyncThunk(
  'csm/getMyRequests',
async (gridCriteria: DataStateChangeEventArgs) => {
  try {
    return await csmService.getMyRequests(gridCriteria);
  } catch (error) {
    console.log('Error: ', error);
  }
});

export const csmMyRequestSlice = createSlice({
  name: 'csmMyRequest',
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder
    .addCase(getMyRequests.pending, (state) => {
      state.isLoading = true;
    })
    .addCase(getMyRequests.fulfilled, (state, action) => {
      
      state.result = action.payload?.myRequests || [];
      state.count = action.payload?.count || 0;

      state.isLoading = false;
      state.isSuccess = true;
    })
    .addCase(getMyRequests.rejected, (state) => {
      
      state.result = [];
      state.count = 0;

      state.isLoading = false;
      state.isError = true;
    })
  },
});

export default csmMyRequestSlice.reducer;

React 组件代码

import { FC, useEffect, useRef, useState } from 'react';
import { Internationalization } from '@syncfusion/ej2-base';
import { ColumnDirective, ColumnsDirective, DataStateChangeEventArgs, Grid, GridComponent } from '@syncfusion/ej2-react-grids';
import { Inject, Page, Sort, Filter, FilterSettingsModel, Resize, ColumnChooser, DetailRow } from '@syncfusion/ej2-react-grids';
import { useAppDispatch, useAppSelector } from '../../../hooks/redux/hooks';
import styles from './MyRequests.component.module.scss';
import { getMyRequests } from '../csmMyRequestSlice';
import { IconButton, styled, Tooltip, tooltipClasses, TooltipProps } from '@mui/material';
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
import { ServiceRequestResult } from '../models/ServiceRequestResult.interface';

let instance = new Internationalization();

const MyRequestsComponent: FC = () => {

  const dispatch = useAppDispatch();

  const { isLoading, result, count, isSuccess } = useAppSelector((state) => state.csmMyRequestReducer);

  let initialMyRequests = { result: [], count: 0 };
  const [myRequests, setMyRequests] = useState<ServiceRequestResult>(initialMyRequests);

  const pageSettings = {
    pageSize: 10,
    pageSizes: ["10", "20", "30", "40", "50"]
  };

  const sortSettings = {
    columns: []
  };

  const columnChooserSettings = {
    hideColumns: [
      "Contact",
      "Request Subtype",
      "Reference",
      "Sys. Logged Date",
      "Sys. Closed Date"
    ]
  };

  let myGridInstanceRef: Grid | null;

  const format = (value: Date) => {
    return instance.formatDate(value, { skeleton: 'yMd', type: 'date' });
  };

  const dataBound = () => {
  }

  const dataStateChange = (gridCriteria: DataStateChangeEventArgs) => {

    if (myGridInstanceRef && gridCriteria.action) {
      const requestType = gridCriteria.action.requestType;

      switch (requestType) {
        case 'paging':
        case 'sorting':
          dispatch(getMyRequests(gridCriteria));
          break;
      }
    }
  };

  
  const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => (
    <Tooltip {...props} classes={{ popper: className }} />
  ))({
    [`& .${tooltipClasses.tooltip}`]: {
      maxWidth: 500,
      fontSize: 13,
      color: 'white',
    },
  });

  function gridDetailTemplate(props: any) {
    return (
      <CustomWidthTooltip title={props.Detail}><p className={`${styles['RequestDetailText']}`}>Detail: {' '}{props.Detail}</p></CustomWidthTooltip>
    );
  }
  let template: any = gridDetailTemplate;

  const columnChooserClick = (event: React.MouseEvent<HTMLElement>) => {
    if (myGridInstanceRef) {
      myGridInstanceRef.hideSpinner(); //Forced hide of spinner here
      myGridInstanceRef.columnChooserModule.openColumnChooser();
    }
  };

  useEffect(() => {
    if (myGridInstanceRef) {
      if (isLoading) {
        console.log('is Loading show spinner'); //Goes through here but spinner doesn't display
        myGridInstanceRef.showSpinner();
      } else {
        console.log('not Loading hide spinner'); //Who knows if it gets hidden as  it never gets displayed
        myGridInstanceRef.hideSpinner();
      }
    }
  }, [isLoading])

  useEffect(() => {
    if (myGridInstanceRef && isSuccess) {
      setMyRequests({ result: result, count: count });
    }
  }, [result, isSuccess])

  useEffect(() => {
    if (myGridInstanceRef) {
      columnChooserSettings.hideColumns.forEach((field) => {
        myGridInstanceRef!.hideColumns(field);
      });

      const gridCriteria = { skip: 0, take: 10 };
      dispatch(getMyRequests(gridCriteria));
    }
  }, [])

  return (
    <div className={`${styles['RequestSection']}`}>
      <legend className={`${styles['RequestLegend']}`}>My Requests:
        <Tooltip title="Show/Hide Columns">
          <IconButton
            className={`${styles['ColumnChooser']}`}
            onClick={columnChooserClick}
            size="small"
          >
            <ViewColumnIcon />
          </IconButton>
        </Tooltip>
      </legend>

      <div className={`${styles['RequestGridContainer']}`}>

        <GridComponent
          ref={(g) => (myGridInstanceRef = g)}
          dataSource={myRequests}
          allowPaging={true} pageSettings={pageSettings}
          allowSorting={true} allowMultiSorting={true} sortSettings={sortSettings}
          allowResizing={true}
          allowReordering={true}
          showColumnChooser={true}
          detailTemplate={template.bind(this)}
          dataBound={dataBound.bind(this)}
          dataStateChange={dataStateChange.bind(this)}
          height='100%'
        >
          <ColumnsDirective>
            <ColumnDirective field='ServiceRequestTag' headerText='Request #' />
            <ColumnDirective field='Caller.Name' headerText='Caller' />
            <ColumnDirective field='Source' />
            <ColumnDirective field='Contact.ContactName' headerText='Contact' />
            <ColumnDirective field='ServiceType.ServiceTypeName' headerText='Service Type' />
            <ColumnDirective field='ServiceRequestType.ServiceRequestTypeName' headerText='Request Type' />
            <ColumnDirective field='ServiceRequestSubtype.ServiceRequestSubtypeName' headerText='Request Subtype' />
            <ColumnDirective field='Poi.Address' headerText='POI Address' />
            <ColumnDirective field='Poi.CityTown' headerText='POI City/Town' />
            <ColumnDirective field='ReferenceNumbers' headerText='Reference' />
            <ColumnDirective field='OwnerName' headerText='Owner' />
            <ColumnDirective field='Status.StatusName' headerText='Status' width='100' />
            <ColumnDirective field='LoggedByName' headerText='Logged By' />
            <ColumnDirective field='LoggedDate' headerText='Logged Date' type='datetime' format='dd MMM yyyy HH:mm' />
            <ColumnDirective field='SystemLoggedDate' headerText='Sys. Logged Date' type='datetime' format='dd MMM yyyy HH:mm' />
            <ColumnDirective field='ClosedByName' headerText='Closed By' />
            <ColumnDirective field='ClosedDate' headerText='Closed Date' type='datetime' format='dd MMM yyyy HH:mm' />
            <ColumnDirective field='SystemClosedDate' headerText='Sys. Closed Date' type='datetime' format='dd MMM yyyy HH:mm' />
            <ColumnDirective field='DueDate' headerText='Due Date' type='datetime' format='dd MMM yyyy HH:mm' />
          </ColumnsDirective>
          <Inject services={[Page, Sort, Resize, ColumnChooser, DetailRow]} />
        </GridComponent>
      </div>
    </div>
  )
}

export default MyRequestsComponent;

问题分析与修复方案

1. 初始请求Spinner不显示

原因:组件初始化时,Grid实例myGridInstanceRef还未完成挂载,此时监听isLoading的useEffect执行时,Grid实例不存在,导致showSpinner()调用无效;同时初始请求的useEffect依赖为空,可能在Grid实例未就绪时就触发了请求。

修复:

  • 调整初始请求的时机,将myGridInstanceRef作为依赖,确保Grid实例就绪后再执行请求:
useEffect(() => {
  if (!myGridInstanceRef) return;
  
  // 先隐藏默认Spinner
  myGridInstanceRef.hideSpinner();
  
  // 执行列隐藏逻辑
  columnChooserSettings.hideColumns.forEach((field) => {
    myGridInstanceRef.hideColumns(field);
  });

  const gridCriteria = { skip: 0, take: 10 };
  dispatch(getMyRequests(gridCriteria));
}, [myGridInstanceRef])
  • 更新监听isLoading的useEffect,添加myGridInstanceRef作为依赖,确保实例存在时再操作Spinner:
useEffect(() => {
  if (!myGridInstanceRef) return;
  
  isLoading ? myGridInstanceRef.showSpinner() : myGridInstanceRef.hideSpinner();
}, [isLoading, myGridInstanceRef])

2. 数据加载后Spinner无法消失(关联detail template)

原因:

  • 冗余的myRequests状态与Redux状态同步存在时序问题,导致Grid数据更新时机与Spinner状态不同步;
  • detailTemplate使用bind(this)会导致每次渲染都生成新的函数,触发Grid内部不必要的重渲染,干扰Spinner状态;
  • 未利用Grid的dataBound事件确保数据渲染完成后隐藏Spinner。

修复:

  • 移除本地myRequests状态,直接使用Redux中的result和count作为Grid数据源,减少状态同步复杂度:
// 移除本地状态定义
// const [myRequests, setMyRequests] = useState<ServiceRequestResult>(initialMyRequests);

// Grid组件中直接使用Redux状态
<GridComponent
  ref={(g) => (myGridInstanceRef = g)}
  dataSource={{ result, count }}
  // ...其他属性
>
  • 简化detailTemplate的传递方式,无需bind(this):
<GridComponent
  // ...其他属性
  detailTemplate={gridDetailTemplate}
  dataBound={dataBound}
>
  • 利用dataBound事件确保数据渲染完成后隐藏Spinner:
const dataBound = () => {
  if (myGridInstanceRef) {
    myGridInstanceRef.hideSpinner();
  }
}
  • 在Redux的rejected分支中重置isSuccess状态,避免状态异常:
.addCase(getMyRequests.rejected, (state) => {
  state.result = [];
  state.count = 0;
  state.isLoading = false;
  state.isError = true;
  state.isSuccess = false; // 新增该行
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 15:30:53