Bryntum
5 March 2025

Using a Bryntum TreeGrid with the GitHub GraphQL API: Lazy loading, on-demand data fetching, and remote filtering

When working with large data sets in a grid, lazy loading and on-demand data fetching are two common strategies to […]

When working with large data sets in a grid, lazy loading and on-demand data fetching are two common strategies to improve performance. The Bryntum Grid lazy data loading feature, also known as infinite scrolling, lets you load data in chunks as a user scrolls down the grid.

If you have data with a tree structure, where there are parent-child relationships, you can use the Bryntum TreeGrid to load child data on demand when a parent node is expanded.

In this tutorial, we’ll show you how to create a React Bryntum TreeGrid, load child data on demand, and use the lazy-load feature. We’ll use the GitHub GraphQL API to get a large dataset: the issues and issue comments in the VS Code GitHub repository:

This is the second article in a two-part series covering lazy loading. Part 1, Infinite scrolling with the new Bryntum lazyLoad feature, also shows you how to implement infinite scrolling using the Bryntum Grid’s lazy-loading feature. It covers important considerations when using lazyLoad, configuration, the expected server response, remote filtering, and remote sorting. A vanilla JavaScript Bryntum Grid is used with an Express server that serves a large dataset (100,000 records) from a JSON file.

Getting started

Clone the Bryntum TreeGrid app starter repository. The completed-grid branch contains the code for the completed tutorial.

The starter repository is a Next.js app with a Grid component, where we’ll create the Bryntum TreeGrid. The Grid component is dynamically imported in the GridWrapper component with server-side rendering turned off. We need to do this because Bryntum components are client-side only. The GridWrapper component is rendered in the Home page.

Install the dependencies using the following command:

npm install

Before we make a Bryntum TreeGrid, let’s learn how to use the GitHub GraphQL API, which will be our data source.

Using the GitHub GraphQL API

The GitHub GraphQL API lets you query GitHub repository data. The API requires authentication, which you can do using a GitHub personal access token. To create one that can read public repository data, generate a fine-grained personal access token in your GitHub Settings menu and set the Repository access to “Public Repositories (read-only)”.

Using the GitHub GraphQL API explorer and pagination

The GitHub GraphQL API Explorer lets you test queries. Add the following query to the explorer to get issues from a GitHub repo:

query ($first: Int!, $owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    issues(first: $first) {
      totalCount
      edges {
        node {
          id
          author {
            login
          }
          number
          title
          comments {
            totalCount
          }
        }
      }
      pageInfo {
        endCursor
        hasNextPage
      }
    }
  }
}

This query uses three variables:

We query the edges, which represent the connections to individual issue nodes. For each node, we fetch details including the idauthortitle, and the totalCount of comments.

The pageInfo object provides pagination details like the endCursor and hasNextPage values. Each returned edge has a cursor that’s used as a marker for pagination. The endCursor is the cursor for the last issue returned.

Add the following variables below the query to get the first 100 issues in the VS Code GitHub repository:

{
  "first": 100,
  "owner": "microsoft",
  "name": "vscode"
}

Run the query by clicking the pink Execute query button. You’ll see the returned data in the panel to the right of the query:

You can get the next 100 issues by adding an after argument to the issues field in the query and setting its value to the returned endCursor string.

Adding your GitHub personal access token to the starter repository

In your cloned Bryntum TreeGrid app starter repository, create a .env file in the root directory and add your GitHub personal access token to it:

GITHUB_PERSONAL_ACCESS_TOKEN=<your-access-token>

Creating and configuring a basic Bryntum TreeGrid

First, install the Bryntum Grid component by following the instructions to access the Bryntum npm registry, and then install the Grid and React Grid wrapper.

Next, replace the code in the src/components/Grid.tsx file with the following lines of code to create a basic Bryntum TreeGrid:

import { useRef } from 'react';
import { BryntumTreeGrid, BryntumTreeGridProps } from '@bryntum/grid-react';
export default function Grid() {
    const gridRef = useRef<BryntumTreeGrid>(null);
    const gridProps: BryntumTreeGridProps = {
        animateTreeNodeToggle : true,
        data : [
            {
                id       : 1,
                name     : 'ABBA',
                iconCls  : 'b-icon b-fa-users',
                born     : null,
                expanded : true,
                children : [
                    { id : 11, name : 'Anni-Frid', born : 1945, iconCls : 'b-icon b-fa-user' },
                    { id : 12, name : 'Bjorn', born : 1945, iconCls : 'b-icon b-fa-user' },
                    { id : 13, name : 'Benny', born : 1946, iconCls : 'b-icon b-fa-user' },
                    { id : 14, name : 'Agnetha', born : 1950, iconCls : 'b-icon b-fa-user' }
                ]
            },
            {
                id       : 2,
                name     : 'Roxette',
                iconCls  : 'b-icon b-fa-users',
                born     : null,
                children : [
                    { id : 21, name : 'Per', born : 1959, iconCls : 'b-icon b-fa-user' },
                    { id : 22, name : 'Marie', born : 1958, iconCls : 'b-icon b-fa-user' }
                ]
            }
        ],
        columns : [
            { type : 'tree', field : 'name', text : 'Name', flex : 1 },
            { type : 'number', field : 'born', text : 'Born', flex : 1, format : '0'  }
        ]
    };
    return (
        <BryntumTreeGrid {...gridProps} ref={gridRef} />
    );
}

We define the grid configs using the gridProps variable and pass them in as props to the BryntumTreeGrid wrapper component, which encapsulates the Bryntum TreeGrid. The config has some example inline tree data. The TreeGrid must be configured with one TreeColumn with type set to tree. The gridRef is used to access the Bryntum TreeGrid instance.

Replace the styling in the src/app/globals.css file with the following:

@import "@bryntum/grid/grid.stockholm.css";
* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}
body {
  font-family: Poppins, "Open Sans", Helvetica, Arial, sans-serif;
}
#app {
  height: 100vh;
}

Run the local development server using the following command:

npm run dev

You’ll see a basic Bryntum Tree Grid:

Creating a custom GitHub issues data model

Let’s create a custom Bryntum Grid data model for the GitHub issues. Create a lib folder in the src folder. Create a GitHubIssueModel.ts file in the lib folder and add the following lines of code to it:

import { GridRowModel } from '@bryntum/grid';

export class GitHubIssueModel extends GridRowModel {
    static get fields() {
        return [
            'title',
            {
                name : 'number',
                type : 'number'
            },
            'author'
        ];
    }

}

The modelClass is used to define the model fields in the data store. The simplified GitHubIssue custom class for GitHub issues extends the default GridRowModel.

Creating a GitHub issue store with lazy loading

Create a storeConfig.ts file in the src folder and add the following lines of code to it:

import { AjaxStore } from '@bryntum/grid';
import { GitHubIssueModel } from './lib/GitHubIssueModel';
export function createGitHubIssueStore(
): AjaxStore {
    return new AjaxStore({
        tree       : true,
        modelClass : GitHubIssueModel,
        readUrl    : '/api/read',
        lazyLoad : {
            chunkSize : 100 // default value
        },
        autoLoad : true
    });
}

We use a function because we’ll pass in a React state variable to the store config later. A Store is a data container that holds the grid data. An AjaxStore is a store that uses the Fetch API to read data and sync data changes to a remote server. We set the API route URL that we’ll read data from using the readURL property. Setting the autoLoad property to true means that the data is fetched on page load. We’ll create a Next.js API route for the read URL.

The lazyLoad property enables the lazy loading of data. The number of records fetched on each lazy load is the “chunk size”. The store keeps track of what data has been loaded, and when a user scrolls far enough down the grid, the next chunk of data is fetched.

When a fetch request is made from a tree grid, startIndexcount, and parentId are added as URL parameters to the request. The startIndex parameter is the index where the server should start reading data from the database and count is the chunkSize. If requests are made from the top level of the tree grid, the parentId is root.

Creating the Bryntum TreeGrid configuration file

Create a gridConfig.ts file in the src folder and add the following lines of code to it:

import { AjaxStore, StringHelper } from '@bryntum/grid';
import { BryntumTreeGridProps } from '@bryntum/grid-react';
type GridConfigOptions = {
  store: AjaxStore;
};
export function createGridConfig(
    { store }: GridConfigOptions
): BryntumTreeGridProps {
    return {
        animateTreeNodeToggle : true,
        summaryFeature        : true,
        cellTooltipFeature    : {
            hoverDelay      : 300,
            textContent     : true,
            tooltipRenderer : ({ record }) => {
                if (record.getData('number')) return null;
                return StringHelper.encodeHtml(record.getData('title'));
            }
        },
        sortFeature : false,
        readOnly    : true,
        store,
        columns     : [
            {
                type            : 'number',
                field           : 'number',
                text            : 'Issue #',
                width           : 180,
                filterable      : false,
                tooltipRenderer : false,
                sum             : 'count',
                summaryRenderer : ({ sum }) => `Total loaded: ${sum}`
            },
            {
                id    : 'IssuesAndComments',
                type  : 'tree',
                field : 'title',
                flex  : 1
            },
            {
                field           : 'author',
                text            : 'Author',
                width           : 210,
                filterable      : false,
                tooltipRenderer : false
            }
        ],
        tbar : [
            {
                type  : 'container',
                style : StringHelper.xss`width: 500px; color: black;`,
                items : [{
                    type  : 'display',
                    value : 'GitHub Issues and comments for the VS Code repository'
                }]
            },
        ]
    };
}

We pass the lazy-loading data store to the grid config. When lazy loading data, remote sorting and filtering is enabled and local sorting and filtering is disabled. We disable the sortFeature, as we won’t implement it in this tutorial. The summaryFeature lets us see the number of issues loaded at the bottom of the number column. The cellTooltipFeature lets us see long issue titles in a tooltip.

The tbar configures the Toolbar displayed at the top of the Grid, which reads, “GitHub Issues and comments for the VS Code repository”.

Adding the Bryntum Grid config to the Bryntum TreeGrid

Replace the code in the src/components/Grid.tsx file with the following lines of code:

import { useMemo, useRef } from 'react';
import { createGridConfig } from '@/gridConfig';
import { createGitHubIssueStore } from '@/storeConfig';
import { BryntumTreeGrid } from '@bryntum/grid-react';
export default function Grid() {
    const gridRef = useRef<BryntumTreeGrid>(null);
    const store = useMemo(() =>
        createGitHubIssueStore()
      ,[]);
    const gridProps = useMemo(() =>
        createGridConfig({ store }),
    [store]
    );
    return (
        <BryntumTreeGrid {...gridProps} ref={gridRef} />
    );
}

We use the createGitHubIssueStore and createGridConfig functions to create the grid configs that are passed to the Bryntum TreeGrid as props. The store and gridProps are memoized to prevent unnecessary recreations of these complex objects on rerenders. This optimization ensures they’re only regenerated when their dependencies change, improving performance and preventing potential issues with event listeners.

Creating a Next.js read API route to return data chunks from the GitHub GraphQL API

In the src/app/ folder, create an api folder. In api, create a read folder. In read, create a route.ts file and add the following lines of code to it:

import { type NextRequest } from 'next/server';
type Variables = {
    first: number;
    queryString?: string;
    after?: string;
    issueId?: string | null;
};
type GitHubIssue = {
  id: string;
  number: number;
  title: string;
  author?: {
    login: string;
  } | null;
  comments: {
    totalCount: number;
  };
}
type IssueEdge = {
  node: GitHubIssue;
}
type IssueData = {
  id: string;
  number: number;
  author: string;
  title: string;
  children: boolean;
  remoteChildCount: number;
}
export async function GET(request: NextRequest) {
    const searchParams = request.nextUrl.searchParams;
    const count = searchParams.get('count');
    const filters = searchParams.get('filters');
    const endCursor = searchParams.get('endCursor') || 'null';
    const parentId = searchParams.get('parentId');
    if (!count) {
        return Response.json({
            success : false,
            message : 'Missing count parameter'
        }, {
            status : 400
        });
    }
    const githubRepoOwner = 'microsoft';
    const githubRepoName = 'vscode';
    let query = '';
    const variables: Variables = {
        first : parseInt(count) || 100
    };
    const baseQuery = `repo:${githubRepoOwner}/${githubRepoName} is:issue`;
    variables.queryString = baseQuery;
    // Lazyload issues
    if (!filters && parentId === 'root') {
        const afterStr = endCursor !== 'null' && endCursor !== 'undefined' ? `${endCursor}` : '';
        variables.after = afterStr;
        query = `
          query ($first: Int!, $after: String) {
            repository(owner: "${githubRepoOwner}", name: "${githubRepoName}") {
              issues(first: $first, after: $after) {
                totalCount
                edges {
                  node {
                    id
                    author {
                      login
                    }
                    number
                    title
                    comments {
                      totalCount
                    }
                  }
                }
                pageInfo {
                  endCursor
                  hasNextPage
                }
              }
            }
          }
        `;
        try {
            const response = await fetch('https://api.github.com/graphql', {
                method  : 'POST',
                headers : {
                    'Content-Type'  : 'application/json',
                    'Authorization' : `bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`
                },
                body : JSON.stringify({ query, variables })
            });
            const rawData = await response.json();
            const issues = rawData.data.repository.issues;
            let newEndCursor = null;
            if (issues.pageInfo.hasNextPage) {
                newEndCursor = issues.pageInfo.endCursor;
            }
            const data: IssueData[] = issues.edges.map((edge: IssueEdge): IssueData => {
                const issue: GitHubIssue = edge.node;
                return {
                    id               : issue.id,
                    number           : issue.number,
                    author           : issue?.author ? issue.author.login : 'Unknown',
                    title            : issue.title,
                    children         : issue.comments.totalCount > 0,
                    remoteChildCount : issue.comments.totalCount
                };
            });
            return Response.json({
                data,
                total     : issues.totalCount,
                endCursor : newEndCursor
            });
        }
        catch (error) {
            console.error(error);
            return Response.json({
                success : false,
                message : 'Failed to fetch issues'
            }, {
                status : 400
            });
        }
    }
}

The GET request handler gets the URL parameters sent by the Bryntum TreeGrid using the searchParams.get() method.

If no filters are applied and parentId is root, a GraphQL query string gets the first 100 issues from the VS Code GitHub repository, just as we did in the GitHub GraphQL API Explorer. The fetch API is used to make a query to the GitHub GraphQL API using the query string and the variables for the query. The GitHub personal access token is added to the 'Authorization' header using an environment variable.

The server returns an object with a data property containing the GitHub issues. The total property is the total number of records in the database, which the Bryntum TreeGrid uses to determine when to stop making requests for data as the user nears the bottom of the grid. The endCursor property is used for pagination. We’ll create a React ref in the client-side Grid component to save the endCursor value, which we’ll send with each load request.

The returned data also includes a remoteChildCount field for each issue, indicating the number of comments. When using a tree store, each parent must have a remoteChildCount field containing the count of all the parent’s children (including nested grandchildren, if there are any).

Open http://localhost:3000. You should see a Bryntum TreeGrid with the first 100 issues of the VS Code GitHub repository:

If you scroll down, a GET request is made for the next 100 issues but it fails to load because we have not saved the new endCursor value on the client and sent it with the next lazy-load request. Let’s get the lazy loading working now.

Saving the pagination end cursor in the Bryntum TreeGrid component

Add the following import to the src/storeConfig.ts file:

import { RefObject } from 'react';

Add the following endCursorRef parameter to the createGitHubIssueStore function:

endCursorRef: RefObject<string | null>

Add the following event handler properties to the returned AjaxStore:

onBeforeLoad(event) {
    (event.params as
        {
            endCursor: string | null;
        }
    ).endCursor = endCursorRef.current;
},
onAfterRequest({ response }) {
    const res = response as OnAfterRequest;
    // end cursor for lazy loading issues
    if (res.parsedJson.commentEndCursor === undefined) {
        endCursorRef.current = res.parsedJson.endCursor;
    }
},

Add the OnAfterRequest type to the top of the file:

type OnAfterRequest = Response & {
  parsedJson: {
    endCursor: string;
    commentEndCursor: string;
    total: number;
  };
};

The onBeforeLoad event handler is used to add the endCursorRef value as a parameter to the load request. The onAfterRequest event handler is used to set the endCursorRef value to the new endCursor value returned by the load API route.

In src/components/Grid.tsx file, add the following React ref to the Grid component:

const endCursorRef = useRef<string | null>(null);

TheendCursor value will be saved in this ref.

Add the endCursorRef as an argument to the createGitHubIssueStore function used to create the store:

const store = useMemo(() =>
    createGitHubIssueStore(
        endCursorRef
    ), []);

Now when you scroll down the TreeGrid, more issues will be fetched from the GitHub GraphQL API:

Using lazy-load event handlers to indicate loading state

The lazy load feature has two lazy-load-specific event handlers:

We’ll use these event handlers in the store to set a React state variable that stores the lazy-loading state.

Import the useState React Hook in src/components/Grid.tsx file:

import { useState } from 'react';

Add the following state variable declaration to the Grid component:

const [networkValue, setNetworkValue] = useState<NetworkValueTypes>({
    text  : 'Idle',
    color : 'green'
});

Add the following type for the state object at the top of the file:

export type NetworkValueTypes = {
  text: 'Idle' | 'Loading' | 'Committing';
  color: 'green' | 'blue' | 'red';
};

Add the state setter setNetworkValue as an argument to the createGitHubIssueStore function call:

    const store = useMemo(() =>
        createGitHubIssueStore(
            endCursorRef,
            setNetworkValue // <---
        ),
    []
    );

Add the networkValue state in the object argument of the createGridConfig function call and add networkValue to the gridProps useMemo dependency list.

    const gridProps = useMemo(() =>
        createGridConfig({
            networkValue,    // <---
            store }),
    [networkValue, store]    // <---
    );

In the src/storeConfig.ts file, import NetworkValueTypes:

import { NetworkValueTypes } from './components/Grid';

Add the setNetworkValue state setter as a parameter in the createGitHubIssueStore function:

setNetworkValue: (value: NetworkValueTypes) => void

Add the following listeners property to the AjaxStore:

listeners : {
    lazyLoadStarted() {
        setNetworkValue({
            text  : 'Loading',
            color : 'blue'
        });
    },
    lazyLoadEnded() {
        setNetworkValue({
            text  : 'Idle',
            color : 'green'
        });
    }
},

In the src/gridConfig.ts file, import NetworkValueTypes:

import { NetworkValueTypes } from './components/Grid';

Add networkValue to the GridConfigOptions type:

type GridConfigOptions = {
  networkValue: NetworkValueTypes;
  store: AjaxStore;
};

Pass networkValue as a parameter to the createGridConfig function:

networkValue,

Add the following item to the tbar config array:

{
    type  : 'container',
    style : StringHelper.xss`width: 220px; margin-left: auto; color: ${networkValue.color}`,
    items : [{
        type  : 'display',
        value : `Network status: ${networkValue.text}`
    }]
}

The Bryntum TreeGrid toolbar will now display the lazy-loading state:

Dynamically loading child nodes: Issue comments

When a tree parent node is expanded by clicking the right arrow on the left side of a row, a load request for the child nodes is made. The parentId in the load request parameters is the id of the parent node. Let’s first update the read API route to lazy load issue comments.

Creating a GraphQL query to lazy load issue comments in the read API route.

In the src/app/api/read/route.ts file, add the following line of code at the top of the GET request handler to get the commentEndCursor URL parameter that we’ll send using the Bryntum TreeGrid:

const commentEndCursor = searchParams.get('commentEndCursor');

Add the following if block below the if block where there are no filters and the parentId is root:

// Dynamically load comments
if (parentId !== 'root') {
    variables.issueId = parentId;
    // Use commentEndCursor if valid; otherwise, start fresh
    const effectiveCommentEndCursor = (commentEndCursor && commentEndCursor !== 'null' && commentEndCursor !== 'undefined')
        ? commentEndCursor
        : null;
    if (effectiveCommentEndCursor) {
        variables.after = effectiveCommentEndCursor;
    }
    query = `
      query ($issueId: ID!, $first: Int!, $after: String) {
        node(id: $issueId) {
          ... on Issue {
            comments(first: $first, after: $after) {
              totalCount
              edges {
                node {
                  id
                  body
                  createdAt
                  author {
                    login
                  }
                }
              }
              pageInfo {
                endCursor
                hasNextPage
              }
            }
          }
        }
      }
  `;
    try {
        const response = await fetch('https://api.github.com/graphql', {
            method  : 'POST',
            headers : {
                'Content-Type'  : 'application/json',
                'Authorization' : `bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`
            },
            body : JSON.stringify({ query, variables })
        });
        const rawData = await response.json();
        const comments = rawData.data.node.comments;
        let newCommentEndCursor = null;
        if (comments.pageInfo.hasNextPage) {
            newCommentEndCursor = comments.pageInfo.endCursor;
        }
        const data: MappedComment[] = comments.edges.map((edge: CommentEdge): MappedComment => {
            const comment: Comment = edge.node;
            return {
                id     : comment.id,
                author : comment?.author ? comment.author?.login : 'Unknown',
                title  : comment.body
            };
        });
        return Response.json({
            data,
            commentEndCursor : newCommentEndCursor
        });
    }
    catch (error) {
        console.error(error);
        return Response.json({
            success : false,
            message : 'Failed to fetch comments'
        }, {
            status : 400
        });
    }
}

Add the following types to the top of the file:

type Comment = {
  id: string;
  body: string;
  createdAt: string;
  author?: {
    login: string;
  } | null;
}
type CommentEdge = {
  node: Comment;
}
type MappedComment = {
  id: string;
  author: string;
  title: string;
}

We use commentEndCursor as a query variable to query the issue comments. The comments query fetches a paginated list of comments for a specific issue using the issue id. Because the returned node could be of any type, the ... on Issue inline fragment is used to ensure the node is treated as an issue. This allows you to query issue-specific fields such as comments.

The newCommentEndCursor variable value is set using pageInfo.endCursor and returned in the response as the commentEndCursor property.

Saving a pagination end cursor for comments in the React Bryntum TreeGrid component

In src/components/Grid.tsx file, add the following React ref to the Grid component:

const commentEndCursorRef = useRef<string | null>(null);

This React ref saves the end cursor for comment queries.

Pass the two end cursor refs as arguments to the createGitHubIssueStore function call:

gridRef,
commentEndCursorRef,

In the src/storeConfig.ts file, add the following import:

import { BryntumTreeGrid } from '@bryntum/grid-react';

Add the following parameters to the createGitHubIssueStore function:

gridRef: RefObject<BryntumTreeGrid | null>,
commentEndCursorRef: RefObject<string | null>,

Replace the onBeforeLoad and onAfterRequest event handlers with the following:

onBeforeLoad(event) {
    (event.params as
        {
            endCursor: string | null;
            commentEndCursor: string | null
        }
    ).endCursor = endCursorRef.current;
    (event.params as {
      endCursor: string | null;
      commentEndCursor: string | null
    }).commentEndCursor = commentEndCursorRef.current;
},
onAfterRequest({ response }) {
    const res = response as OnAfterRequest;
    // end cursor for lazy loading issues
    if (res.parsedJson.commentEndCursor === undefined) {
        endCursorRef.current = res.parsedJson.endCursor;
    }
    // end cursor for lazy loading comments
    else {
        commentEndCursorRef.current = res.parsedJson.commentEndCursor;
    }
    if (gridRef.current?.instance && res.parsedJson.total) {
        // @ts-expect-error getById is not in the type definition
        const column = gridRef.current.instance.columns.getById('IssuesAndComments');
        if (column) {
            column.set({
                text : `Total issues: ${res.parsedJson.total}`
            });
        }
    }
},

The onBeforeLoad event handler is used to add the end cursor refs as parameters in the load request. The onAfterRequest event handler is used to set the end cursor ref values to the new values returned by the load API route.

These two end cursors let you lazy load comments and issues:

Remote filtering parent data: Issues

Filtering can be configured using the filterFeature. Add the following filterFeature property to the grid config returned by the createGridConfig function in the src/gridConfig.ts file:

filterFeature         : {
    allowedOperators : ['includes'],
    pickerConfig     : {
        showAddFilterButton : false
    }
},

The allowedOperators config lets you limit the allowed operators that populate the filtering sub-menus. For simplicity, we limit the allowed operators to 'includes' only.

When remote filtering is enabled, the filter information is added to load request parameters.

In the src/storeConfig.ts file, add the following property to the AjaxStore:

filterParamName : 'filters',

The filterParamName property is the parameter name used to pass filters when loading remote data.

To add remote filtering functionality to our API route, add the following if block to the GET request handler in the src/app/api/read/route.ts file:

// Filter issues
if (filters && parentId === 'root') {
    const parsedFilter = JSON.parse(filters);
    const filterObj = parsedFilter[0];
    if (filterObj.field === 'title' && filterObj.operator === 'includes') {
        variables.queryString += ` ${filterObj.value} in:${filterObj.field}`;
    }
    // Use endCursor if valid; otherwise, start fresh
    const effectiveEndCursor = (endCursor && endCursor !== 'null' && endCursor !== 'undefined')
        ? endCursor
        : null;
    if (effectiveEndCursor) {
        variables.after = effectiveEndCursor;
    }
    query = `
      query ($queryString: String!, $first: Int!, $after: String) {
        search(query: $queryString, type: ISSUE, first: $first, after: $after) {
          issueCount
          edges {
            node {
              ... on Issue {
                id
                number
                title
                author {
                  login
                }
                comments {
                  totalCount
                }
              }
            }
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
  `;
    try {
        const response = await fetch('https://api.github.com/graphql', {
            method  : 'POST',
            headers : {
                'Content-Type'  : 'application/json',
                'Authorization' : `bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`
            },
            body : JSON.stringify({ query, variables })
        });
        const rawData = await response.json();
        const issues = rawData.data.search;
        let newEndCursor = null;
        if (issues.pageInfo.hasNextPage) {
            newEndCursor = issues.pageInfo.endCursor;
        }
        const data: IssueData[] = issues.edges.map((edge: IssueEdge): IssueData => {
            const issue: GitHubIssue = edge.node;
            return {
                id               : issue.id,
                number           : issue.number,
                author           : issue?.author ? issue.author.login : 'Unknown',
                title            : issue.title,
                children         : issue.comments.totalCount > 0,
                remoteChildCount : issue.comments.totalCount
            };
        });
        return Response.json({
            data,
            total     : issues.issueCount,
            endCursor : newEndCursor
        });
    }
    catch (error) {
        console.error(error);
        return Response.json({
            success : false,
            message : 'Failed to fetch issues'
        }, {
            status : 400
        });
    }
}

Filtering is done using a search query. The query argument in search is the search string to look for. The GitHub GraphQL API has search syntax that allows you to create queries that match specific numbers and words. For example, the following search query string has an in qualifier that restricts the search to issues with a title that includes the word “undefined”:

"repo:microsoft/vscode is:issue undefined in:title"

To see filtering in action, hover over the title column and click the filter icon on the right side of the title column to open the filter popup:

Next steps

To learn more about using Bryntum products with GraphQL, check out our blog post, Create a scheduler using Bryntum, Next.js, Prisma, SQLite, and GraphQL.

You can also learn how to implement remote sorting in the first article of this two-part series on lazy loading in Bryntum Grid: Infinite data Grid scrolling with the new lazy loading feature.

Bryntum

Bryntum Grid