Grid gets stuck in an infinite loop when the filter returns the same results on two consecutive occasions

Hello,

As the title says, when the filter returns the same results for a second consecutive time, it gets stuck in an infinite loop. For instance, if I filter for a value that returns no results, it will work the first time, but if I do it again for a second time the spinning indicator comes up and does not go away. I've checked the incoming response and it does get refreshed on every call. The Grid though remains stuck.

I'm using remote data binding through an api call. All filtering functions are done server-side and I am not using DataManager. The app's framework is Next.js.

Could you please provide some insight into why that may be happening?


9 Replies

RR Rajapandi Ravi Syncfusion Team April 14, 2025 12:23 PM UTC

Hi George,


Greetings from Syncfusion support


Based on your query it appears that you are encountering an issue of Grid gets stuck in an infinite loop when the filter returns same result. Before we start providing solution on your query, we need some more information for our clarification. So please share the below details that would be helpful for us to provide better solution,


1)  Complete Grid Rendering Code (both client-side and server-side): This will help us understand how you are invoking your API during filtering, how server-side filtering is handled, and how the filtered data is bound to the Grid.


2)  Network Tab Screenshots: Please share screenshots of the request payload and server response from your browser’s Network tab. We’d like to review the filter query structure and the format of the returned data.


3)  Issue Demonstration Video: If possible, provide a short video showing the issue along with the steps to reproduce it.


4) State Updates During Filtering: Let us know if you are performing any state updates in the Grid during the filtering process.


5) Script Error Details (if any): If you are encountering any errors in the browser console (JavaScript exceptions or warnings), please share the complete error messages along with a screenshot. These details will help us identify any potential client-side issues.


Regards,

Rajapandi R



GE George April 20, 2025 07:31 AM UTC

Hello,


Unfortunately, I am away and I can't provide the requested details. 

However, I remembered that when I inspected the GridComponent with the React Developer tools, when the grid is working correctly, the GridComponent's dataSource is the object {actionArgs, enablePersistance, result, count, etc}, whereas when it gets stuck to a loop, the dataSource contains only {result, count}. For some reason the filtering events aren't being triggered. I should also add that the grid is in a child component which receives the data as a prop from a parent component.

I hope that this makes sense and can help shed some light.



RR Rajapandi Ravi Syncfusion Team April 22, 2025 02:11 AM UTC

George,


Thanks for your update


Based on your query, it seems that you are using the API to fetch data and handle the Grid actions like filtering, paging, and sorting etc. in your own API service and facing the problem. You can achieve your requirement by using the Custom Binding feature of Grid.


The custom binding feature in the React Grid enables you to manage your own custom API for handling data processing externally and then binding the resulting data to the Grid. This allows you to implement your own custom data logic to your application’s requirements. When using custom binding, the Grid expects the result of the custom logic to be an object with properties result and count. The result property should contain the data to be displayed in the Grid, while the count property indicates the total number of records in the dataset for your application.


The dataStateChange event is triggered whenever you perform actions that modify the state of the grid’s data, such as changing pages, applying sorting, or grouping. This event provides detailed information about the action performed and the current state of the grid, including parameters like page number, sorting details, and filtering criteria.


When filtering operation is performed in the Grid, the dataStateChange event is triggered with following arguments you can handle this in your service and return the result in the required format result and count.


FilterBar


We have already discussed about this in our documentation which can be accessed from the below link,


Documentation:     https://ej2.syncfusion.com/react/documentation/grid/data-binding/remote-data#custom-binding

                                  https://ej2.syncfusion.com/react/documentation/grid/data-binding/remote-data#handling-filtering-operation


Since that the dataSource object differs between working and non-working states, the absence of actionArgs and enablePersistence might point to an issue with how events or lifecycle methods are being executed. Here's a potential approach to address this issue:


1)  Grid Events: Verify that all necessary events for the Grid component are properly captured and handled. This includes ensuring filtering events trigger the correct actions and that state updates occur as expected.


2)  DataSource Structure: By default, in our Grid component, actions such as sorting and filtering trigger the dataStateChange event, which includes the corresponding state action arguments.

Please inspect why the actionArgs and enablePersistence properties are missing in the non-working scenario. Ensure that you are correctly receiving the relevant action arguments in the dataStateChange event and that your service is properly handling the filtering action. Additionally, your server response should include the required properties—such as result and count—along with any necessary state data to maintain functionality during filtering.


3)  Actionargs and enablePersistence: Please ensure on your end that while returning the filter results from your service, the actionArgs and enablePersistence properties are not being handled at the application level in a way that could lead to script errors.


4)  Error Handling: Implement error handling in your API calls and ensure any errors returned from the server are dealt with, such as retrying requests or providing user feedback to prevent infinite loops.


Once these areas are reviewed and adjusted, the issue may be resolved. If you are still facing difficulties, sharing your Grid code snippets related to these aspects once you are able to will help us provide a more precise solution.



GE George April 22, 2025 01:19 PM UTC

Hello,


Thanks for getting back to me. Regarding the DataManager example in the custom binding section, I am aware of it and I purposefully decided against using it as it requires all the data from the server. In any case, I am attaching the relevant functions.

    const dataStateChange = async (state: any) => {
      if (state.action.action == 'filter') {

//filterFields is an array containing all the filtered columns, their values and operators

        filterFields = [
          ...filterFields,
          ...state.action.columns.map((filter: any) => {
            return [
              filter.properties.field,
              filter.properties.value,
              filter.properties.operator,
            ];
          }),
        ];
      } else if (state.action.action == 'clearFilter') {
        filterFields = [
          ...filterFields,
          ...state.action.currentFilterObject.parentObj.properties.columns.map(
            (filter: any) => {
              return [
                filter.properties.field,
                filter.properties.value,
                filter.properties.operator,
              ];
            }
          ),
        ];
      }

//sortField is a string containing the sorted column and its direction

      if (state.action.requestType == 'sorting' && state.action.columnName) {
        sortField = `${state.action.columnName},${state.action.direction}`;
      } else {
        sortField = 'false';
      }
      try {
// updateRecords is the parent component's fetch function with filter and sort arguments. It's a simple JS try catch
operation which updates the data passed as a prop to the Grid in the child component.

        updateRecords &&
          updateRecords(
            state.skip,
            sortField,
            filterFields.length > 0 ? JSON.stringify(filterFields) : 'false'
          );
      } catch (err: any) {
        console.error(err);
      }
    };


// Server function returning the filtered results. This checks if there are filters in the GET params.
I am using Prisma ORM.

    if (filter) {
      let records = await prisma.record.findMany({
        where: {
          AND: [
            ...(filter as string[]).map((filterField: any) => {
              return {
                createdBy: { equals: user },
                type: type,
                ...(filterField[0] === 'dueDate'
                  ? {
                      dueDate: {
                        ...(filterField[2] === 'equal'
                          ? { equals: new Date(filterField[1]) }
                          : filterField[2] === 'greaterthan'
                          ? { gt: new Date(filterField[1]) }
                          : filterField[2] === 'greaterthanorequal'
                          ? { gte: new Date(filterField[1]) }
                          : filterField[2] === 'lessthan'
                          ? { lt: new Date(filterField[1]) }
                          : filterField[2] === 'lessthanorequal'
                          ? { lte: new Date(filterField[1]) }
                          : filterField[2] === 'notequal'
                          ? { not: new Date(filterField[1]) }
                          : filterField[2] === 'isnull'
                          ? { equals: null }
                          : { not: null }),
                      },
                    }
                  : filterField[0] === 'paymentDate'
                  ? {
                      paymentDate: {
                        ...(filterField[2] === 'equal'
                          ? { equals: new Date(filterField[1]) }
                          : filterField[2] === 'greaterthan'
                          ? { gt: new Date(filterField[1]) }
                          : filterField[2] === 'greaterthanorequal'
                          ? { gte: new Date(filterField[1]) }
                          : filterField[2] === 'lessthan'
                          ? { lt: new Date(filterField[1]) }
                          : filterField[2] === 'lessthanorequal'
                          ? { lte: new Date(filterField[1]) }
                          : filterField[2] === 'notequal'
                          ? { not: new Date(filterField[1]) }
                          : filterField[2] === 'isnull'
                          ? { equals: null }
                          : { not: null }),
                      },
                    }
                  : filterField[0] === 'googleCalendarDate'
                  ? {
                      googleCalendarDate: {
                        ...(filterField[2] === 'equal'
                          ? { equals: new Date(filterField[1]) }
                          : filterField[2] === 'greaterThan'
                          ? { gt: new Date(filterField[1]) }
                          : filterField[2] === 'greaterthanorequal'
                          ? { gte: new Date(filterField[1]) }
                          : filterField[2] === 'lessThan'
                          ? { lt: new Date(filterField[1]) }
                          : filterField[2] === 'lessthanorequal'
                          ? { lte: new Date(filterField[1]) }
                          : filterField[2] === 'notequal'
                          ? { not: new Date(filterField[1]) }
                          : filterField[2] === 'isnull'
                          ? { equals: null }
                          : { not: null }),
                      },
                    }
                  : filterField[0] === 'billIssuerOrExpenseType'
                  ? {
                      billIssuerOrExpenseType: {
                        ...(filterField[2] === 'equal'
                          ? { equals: filterField[1] }
                          : filterField[2] === 'contains'
                          ? { contains: filterField[1] }
                          : filterField[2] === 'startsWith'
                          ? { startsWith: filterField[1] }
                          : filterField[2] === 'endsWith'
                          ? { endsWith: filterField[1] }
                          : filterField[2] === 'isempty'
                          ? { equals: '' }
                          : filterField[2] === 'notequal'
                          ? { not: filterField[1] }
                          : { not: '' }),
                      },
                    }
                  : filterField[0] === 'comments'
                  ? {
                      comments: {
                        ...(filterField[2] === 'equal'
                          ? { equals: filterField[1] }
                          : filterField[2] === 'contains'
                          ? { contains: filterField[1] }
                          : filterField[2] === 'startsWith'
                          ? { startsWith: filterField[1] }
                          : filterField[2] === 'endsWith'
                          ? { endsWith: filterField[1] }
                          : filterField[2] === 'isempty'
                          ? { equals: '' }
                          : filterField[2] === 'notequal'
                          ? { not: filterField[1] }
                          : { not: '' }),
                      },
                    }
                  : filterField[0] === 'amount'
                  ? {
                      amount: {
                        ...(filterField[2] === 'equal'
                          ? { equals: parseFloat(filterField[1]) }
                          : filterField[2] === 'greaterThan'
                          ? { gt: parseFloat(filterField[1]) }
                          : filterField[2] === 'greaterthanorequal'
                          ? { gte: parseFloat(filterField[1]) }
                          : filterField[2] === 'lessThan'
                          ? { lt: parseFloat(filterField[1]) }
                          : filterField[2] === 'lessthanorequal'
                          ? { lte: parseFloat(filterField[1]) }
                          : filterField[2] === 'notequal'
                          ? { not: parseFloat(filterField[1]) }
                          : {}),
                      },
                    }
                  : null),
              };
            }),
          ],
        },
        take: 10,
        skip: skip ? skip : 0,
    if (records) {
        return NextResponse.json({
          result: records,
          count: records.length,
        });
      }
    }
  } catch (e: any) {
    console.error('Error:', e);
    return NextResponse.json({
      error: `Something went wrong - ${(e as Error).message}`,
    });
  }
}

Please bear in mind that this isn't the full function but only the filtering part. It may have missing brackets if
copied as is in an IDE. The function as a whole works flawlessly.




SR Sivaranjani Rajasekaran Syncfusion Team April 23, 2025 02:03 PM UTC

Hi George,
Thank you for reaching out and for providing the details of your inquiry.
From your description, it appears you're encountering an issue with Grid filtering while using custom data binding. In scenarios involving custom binding, it's important to manually construct the query string that reflects the user's filtering actions and send it to the backend service appropriately. This ensures the filtering behavior works as expected.
To assist you better, we've created a sample that demonstrates how to handle filtering in custom binding. Below is a detailed explanation of the code:

dataStateChange Method:

This method is triggered when there is a state change in the Grid (such as filtering, sorting, paging, etc.).

Code Example : 

function dataStateChange(state: DataStateChangeEventArgs | any) {    
    if (
      state.action &&
      (state.action.requestType == 'filterchoicerequest' ||
        state.action.requestType == 'filterSearchBegin' ||
        state.action.requestType == 'stringfilterrequest')
    ) {
      state.skip = 0;
      state.take = 1000;
 // Fetch filtered data
      execute(state).then((response: any) => {
        const data = response['result']
        state.dataSource(data);   
      });
    }
    else{
     // Handle other state changes like paging or sorting
      execute(state).then((gridData) => { grid.dataSource = gridData; });
    }
  }

execute Function:

This is a helper function that passes the state object to getData, which constructs and executes the query.

function execute(state: DataStateChangeEventArgs | any): Promise<DataResult> {
  return getData(state);
}

getData Function:

This is the core function where the query is constructed based on filtering, sorting, and paging values, and sent to the backend.

function getData(state: DataStateChangeEventArgs): Promise<DataResult> {
  const pageQuery = `$skip=${state.skip}&$top=${state.take}`; // Paging info

  // Filtering logic
  if (state.where) {
    filterQuery = `&$filter=` +
      state.where.map((obj) => {
        if (obj.isComplex && obj.predicates) {
          // Handle complex filters
          const predicates = obj.predicates.map((predObj) => {
            if (predObj.isComplex && predObj.predicates) {
              return predObj.predicates
                .filter((predicate) => predicate.operator === 'equal')
                .map((predicate) => {
                  const value = typeof predicate.value === 'string' ? `'${predicate.value}'` : predicate.value;
                  return `${predicate.field} eq ${value}`;
                }).join(' or ');
            } else {
              const value = typeof predObj.value === 'string'
                ? `'${predObj.value.toLowerCase()}'`
                : predObj.value;
              return predObj.operator === 'equal'
                ? `${predObj.field} eq ${value}`
                : `${predObj.operator}(tolower(${predObj.field}), '${predObj.value.toLowerCase()}')`;
            }
          }).join(' and ');
          return `(${predicates})`;
        } else {
          // Handle simple filter
          const value = typeof obj.value === 'string'
            ? `'${obj.value.toLowerCase()}'`
            : obj.value;
          return obj.operator === 'equal'
            ? `${obj.field} eq ${value}`
            : `${obj.operator}(tolower(${obj.field}), '${obj.value.toLowerCase()}')`;
        }
      }).join(' and ');
  } else {
    filterQuery = '';
  }

  // Construct the full query URL
  ajax.url = `${BASE_URL}?${pageQuery}${sortQuery}${filterQuery}&$count=true`;

  // Execute the request and parse the response
  return ajax.send().then((response: any) => {
    const data: any = JSON.parse(response);
    return {
      result: data['value'],                   // Data array
      count: parseInt(data['@odata.count'], 10) // Total record count
    };
  });
}

We’ve also attached a sample project that demonstrates this approach in action. This will help you understand how the filtering logic is implemented and how the query is passed to the service.

Payload screenshot while filtering:

Video Demo:




If you're still encountering issues, we kindly request you to share a reproducible sample, a short video demonstration, or the payload details from your side. Or Try to reproduce the issue in our shared sample. This will help us assist you more efficiently.

We look forward to your response!

Attachment: NextJS_Sample_f2f07248.zip


GE George April 26, 2025 09:16 AM UTC

Ηello,


Thank you for taking the time to come up with a working solution. As it turns out, I didn't need it, but it did help me solve this by adding a ref to the grid and assigning the returned data from the api call to the grid's datasource. Everything is working correctly now.


Thanks once again.



RR Rajapandi Ravi Syncfusion Team April 28, 2025 05:11 AM UTC

George,


We are happy to hear that the provided solution was helpful. Please get back to us if you need any other assistance.



AR Ali Raza July 23, 2025 06:07 PM UTC

Thank you for taking the time to come up with a working solution. As it turns out, I didn't need it, but it did help me solve this by adding a ref to the grid and assigning the returned data from the api call to the grid's datasource. Everything is working correctly now. 



RR Rajapandi Ravi Syncfusion Team July 24, 2025 06:13 AM UTC

Ali,


We are happy to hear that the provided solution was helpful. Please get back to us if you need any other assistance.


Loader.
Up arrow icon