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
相关产品推荐
相关产品推荐

