Unleashing Dynamic Data: AG-Grid with GraphQL in React
Welcome to another installment in our AG-Grid React series! Today, we're diving deep into integrating two powerful technologies: AG-Grid, a feature-rich data grid, and GraphQL, a modern query language for your API. Combining AG-Grid with GraphQL in a React application empowers you to build highly efficient, flexible, and performant data-driven user interfaces, especially when dealing with large datasets.
While AG-Grid excels at client-side data manipulation for smaller datasets, real-world enterprise applications often require server-side processing for pagination, sorting, filtering, and more. This is where GraphQL shines, providing a precise and efficient way for your frontend to request exactly the data it needs from the backend.
Why Combine AG-Grid and GraphQL?
The synergy between AG-Grid and GraphQL offers several compelling advantages:
- Efficiency: GraphQL allows you to fetch only the data AG-Grid requires, eliminating over-fetching and under-fetching, leading to faster load times and reduced bandwidth usage.
- Flexibility: Your React components can define the exact shape and fields of data they need, making it easier to adapt to changing UI requirements without backend modifications.
- Performance: By offloading complex operations like sorting and filtering to the server, AG-Grid remains responsive, even with millions of rows. GraphQL facilitates these targeted server requests.
- Type Safety: GraphQL's strong typing system, often paired with tools like Apollo Client, provides compile-time checks and auto-completion, improving developer experience and reducing errors.
Prerequisites
Before we begin, ensure you have a basic understanding of:
- React: Components, Hooks (`useState`, `useEffect`, `useCallback`, `useMemo`).
- AG-Grid: Basic setup, `rowData`, `columnDefs`, and ideally, the concept of Server-Side Row Model.
- GraphQL: How to write queries, understand arguments, and schema concepts.
- Development Environment: Node.js, npm/yarn installed.
Setting Up Your React & GraphQL Environment
First, let's set up a basic React project and install the necessary dependencies:
# Create a new React app (using Vite for speed)
npm create vite@latest ag-grid-graphql-app -- --template react-ts
cd ag-grid-graphql-app
# Install AG-Grid and Apollo Client
npm install ag-grid-community ag-grid-react @apollo/client graphql
# or yarn add ag-grid-community ag-grid-react @apollo/client graphql
# Install AG-Grid styles
npm install ag-grid-community/styles/ag-grid.css ag-grid-community/styles/ag-theme-alpine.css
For this tutorial, we'll assume you have a GraphQL server running and accessible at a specific endpoint. We'll use Apollo Client as our GraphQL client in React, which is a popular and robust choice.
Basic Data Fetching with AG-Grid (Client-Side)
Let's start with the simplest scenario: fetching a small dataset via GraphQL and displaying it in AG-Grid with client-side pagination. This demonstrates the basic integration before moving to server-side complexities.
1. Configure Apollo Client
Wrap your application with an ApolloProvider, providing it with an ApolloClient
instance configured with your GraphQL endpoint.
// src/App.tsx
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '@apollo/client';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
// Replace with your GraphQL server endpoint
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql', // Example: your GraphQL server endpoint
cache: new InMemoryCache(),
});
// Define a simple GraphQL query
const GET_PRODUCTS = gql`
query GetProducts {
products {
id
name
price
category
}
}
`;
interface Product {
id: string;
name: string;
price: number;
category: string;
}
const ProductGrid: React.FC = () => {
const { loading, error, data } = useQuery<{ products: Product[] }>(GET_PRODUCTS);
const columnDefs = [
{ field: 'id', headerName: 'ID' },
{ field: 'name', headerName: 'Product Name' },
{ field: 'price', headerName: 'Price' },
{ field: 'category', headerName: 'Category' },
];
if (loading) return <p>Loading products...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<AgGridReact
rowData={data?.products || []}
columnDefs={columnDefs}
pagination={true} // Enable client-side pagination
paginationPageSize={10}
></AgGridReact>
</div>
);
};
const App: React.FC = () => (
<ApolloProvider client={client}>
<h1>Products Grid (Client-Side with GraphQL)</h1>
<ProductGrid />
</ApolloProvider>
);
export default App;
In this setup, useQuery fetches all product data, and AG-Grid handles pagination, sorting,
and filtering entirely on the client side. This is suitable for datasets up to a few thousand rows.
For larger datasets, a server-side approach is essential.
Server-Side Data Source for AG-Grid with GraphQL
For large datasets, AG-Grid's Server-Side Row Model is the solution. It tells AG-Grid to request data blocks, and optionally sorting and filtering parameters, from the server as needed. We'll implement a custom data source that translates AG-Grid's requests into GraphQL queries.
1. Understanding AG-Grid's IServerSideDatasource
The core of the server-side model is the IServerSideDatasource interface, which requires a
getRows(params) method. This method is called by AG-Grid whenever it needs new data.
The params object contains crucial information:
request.startRow&request.endRow: The range of rows AG-Grid is requesting (for pagination).request.sortModel: An array of column IDs and their sort directions.request.filterModel: An object containing filter details for each column.successCallback(rowsThisPage, lastRow): A function to call with the fetched data and total row count.failCallback(): A function to call if there's an error.
2. Designing Your GraphQL Schema for Server-Side Operations
Your GraphQL server needs to be capable of receiving arguments for pagination, sorting, and filtering.
A typical schema might look like this for a products query:
type Query {
products(
offset: Int = 0
limit: Int = 10
orderBy: [ProductOrderByInput!]
where: ProductWhereInput
): ProductConnection!
}
type ProductConnection {
totalCount: Int!
nodes: [Product!]!
}
type Product {
id: ID!
name: String!
price: Float!
category: String!
}
enum SortDirection {
ASC
DESC
}
input ProductOrderByInput {
id: SortDirection
name: SortDirection
price: SortDirection
category: SortDirection
# ... other fields
}
input StringFilterInput {
eq: String
neq: String
contains: String
startsWith: String
endsWith: String
# ... other text filters
}
input NumberFilterInput {
eq: Float
neq: Float
gt: Float
gte: Float
lt: Float
lte: Float
# ... other number filters
}
input ProductWhereInput {
id: StringFilterInput
name: StringFilterInput
price: NumberFilterInput
category: StringFilterInput
# ... other fields
AND: [ProductWhereInput!]
OR: [ProductWhereInput!]
}
This schema defines offset and limit for pagination, orderBy for
sorting, and a flexible where input type for filtering.
3. Implementing GraphQLServerSideDatasource
Now, let's create a custom class that implements IServerSideDatasource and makes GraphQL
requests.
// src/GraphQLServerSideDatasource.ts
import { ApolloClient, gql } from '@apollo/client';
import { IServerSideDatasource, IServerSideGetRowsParams, IServerSideGetRowsRequest } from 'ag-grid-community';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
// Define the GraphQL query for server-side operations
// This query dynamically accepts pagination, sorting, and filtering arguments
const GET_PRODUCTS_SERVER_SIDE = gql`
query GetProductsServerSide(
$offset: Int!
$limit: Int!
$orderBy: [ProductOrderByInput!]
$where: ProductWhereInput
) {
products(offset: $offset, limit: $limit, orderBy: $orderBy, where: $where) {
totalCount
nodes {
id
name
price
category
}
}
}
`;
export class GraphQLServerSideDatasource implements IServerSideDatasource {
private apolloClient: ApolloClient<any>;
constructor(apolloClient: ApolloClient<any>) {
this.apolloClient = apolloClient;
}
getRows(params: IServerSideGetRowsParams) {
console.log('AG-Grid request params:', params.request);
const { startRow, endRow, sortModel, filterModel } = params.request;
// 1. Convert AG-Grid sortModel to GraphQL orderBy argument
const orderBy = sortModel.map(sort => ({
[sort.colId]: sort.sort === 'asc' ? 'ASC' : 'DESC',
}));
// 2. Convert AG-Grid filterModel to GraphQL where argument
// This is a simplified example. A real-world application would need more robust logic
// to handle different filter types (agTextColumnFilter, agNumberColumnFilter, agDateColumnFilter, etc.)
const where: any = {};
Object.keys(filterModel).forEach(key => {
const filter = filterModel[key];
// Example for text filter
if (filter.filterType === 'text') {
const textFilter = filter as any; // Cast to access specific properties
if (textFilter.type === 'contains') {
where[key] = { contains: textFilter.filter };
} else if (textFilter.type === 'equals') {
where[key] = { eq: textFilter.filter };
}
// ... add more text filter types like startsWith, endsWith
}
// Example for number filter
else if (filter.filterType === 'number') {
const numberFilter = filter as any;
if (numberFilter.type === 'equals') {
where[key] = { eq: parseFloat(numberFilter.filter) };
} else if (numberFilter.type === 'greaterThan') {
where[key] = { gt: parseFloat(numberFilter.filter) };
}
// ... add more number filter types
}
// Add logic for date filters, set filters, etc.
});
this.apolloClient
.query<{ products: { totalCount: number; nodes: Product[] } }>({
query: GET_PRODUCTS_SERVER_SIDE,
variables: {
offset: startRow,
limit: endRow - startRow, // Calculate limit from startRow and endRow
orderBy: orderBy.length > 0 ? orderBy : undefined,
where: Object.keys(where).length > 0 ? where : undefined,
},
})
.then(result => {
if (result.errors) {
console.error('GraphQL errors:', result.errors);
params.failCallback();
return;
}
const { totalCount, nodes } = result.data.products;
params.successCallback(nodes, totalCount); // Pass fetched rows and total count
})
.catch(error => {
console.error('Error fetching data from GraphQL:', error);
params.failCallback();
});
}
}
4. Integrating the Datasource with AG-Grid in React
Now, let's update our React component to use this custom server-side datasource.
// src/ServerSideProductGrid.tsx
import React, { useCallback, useMemo, useRef } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { useApolloClient } from '@apollo/client';
import {
ColDef,
IServerSideDatasource,
IServerSideGetRowsParams,
IServerSideGetRowsRequest,
GridReadyEvent
} from 'ag-grid-community';
import { GraphQLServerSideDatasource } from './GraphQLServerSideDatasource';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
const ServerSideProductGrid: React.FC = () => {
const gridRef = useRef<AgGridReact>(null);
const apolloClient = useApolloClient(); // Get Apollo client from context
const columnDefs: ColDef[] = useMemo(() => [
{ field: 'id', headerName: 'ID', sortable: true, filter: 'agTextColumnFilter', filterParams: { buttons: ['apply', 'reset'] } },
{ field: 'name', headerName: 'Product Name', sortable: true, filter: 'agTextColumnFilter', filterParams: { buttons: ['apply', 'reset'] } },
{ field: 'price', headerName: 'Price', sortable: true, filter: 'agNumberColumnFilter', filterParams: { buttons: ['apply', 'reset'] } },
{ field: 'category', headerName: 'Category', sortable: true, filter: 'agTextColumnFilter', filterParams: { buttons: ['apply', 'reset'] } },
], []);
const defaultColDef = useMemo(() => ({
flex: 1,
minWidth: 100,
resizable: true,
floatingFilter: true, // Enable floating filters
}), []);
// AG-Grid server-side model configuration
const getRowId = useCallback((params: any) => params.data.id, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
// Instantiate our custom datasource and set it to the grid
const datasource = new GraphQLServerSideDatasource(apolloClient);
params.api.setServerSideDatasource(datasource);
}, [apolloClient]);
return (
<div className="ag-theme-alpine" style={{ height: 600, width: '100%' }}>
<AgGridReact
ref={gridRef}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowModelType={'serverSide'} // Crucial: tell AG-Grid to use server-side model
serverSideStoreType={'full'} // 'full' or 'partial' depending on desired caching behavior
pagination={true}
paginationPageSize={10}
cacheBlockSize={10} // Number of rows requested per block
animateRows={true}
onGridReady={onGridReady}
getRowId={getRowId} // Required for row pinning, selection, etc.
suppressServerSideInfiniteScroll={false} // Allow infinite scroll for server side
></AgGridReact>
</div>
);
};
export default ServerSideProductGrid;
Finally, update App.tsx to render the ServerSideProductGrid:
// src/App.tsx (Updated)
import React from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import ServerSideProductGrid from './ServerSideProductGrid'; // Import the new component
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
// Replace with your GraphQL server endpoint
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql', // Example: your GraphQL server endpoint
cache: new InMemoryCache(),
});
const App: React.FC = () => (
<ApolloProvider client={client}>
<h1>Products Grid (Server-Side with GraphQL)</h1>
<ServerSideProductGrid />
</ApolloProvider>
);
export default App;
With this setup, AG-Grid will now delegate all data fetching, pagination, sorting, and filtering
logic to your GraphQL server via the GraphQLServerSideDatasource. The cacheBlockSize
and paginationPageSize properties in AG-Grid determine the limit value
sent to your GraphQL endpoint.
Error Handling and Loading States
In a production application, robust error handling and visual feedback for loading states are crucial.
-
Loading Indicators: AG-Grid provides built-in overlay APIs. In your
GraphQLServerSideDatasource'sgetRowsmethod, you can callparams.api.showLoadingOverlay()before the GraphQL call andparams.api.hideOverlay()in bothsuccessCallbackandfailCallback. -
Error Messages: Display user-friendly error messages if the GraphQL call fails.
The
failCallback()is the place to trigger this. You might also want to log detailed errors to your console or an error monitoring service.
Best Practices and Advanced Topics
- Debouncing Requests: For rapid user interactions (e.g., typing in a filter box), AG-Grid can send many `getRows` requests. Implement debouncing in your datasource's `getRows` method to avoid overwhelming your server with unnecessary requests.
- Optimistic UI: For actions like inline cell editing, consider using GraphQL mutations with optimistic updates. This provides immediate visual feedback to the user while the actual server operation completes in the background.
- Caching: Apollo Client's normalized cache is powerful. Understand how it works with your GraphQL schema to ensure data consistency and reduce redundant network requests. For server-side row models, AG-Grid also has its own caching mechanism (`serverSideStoreType`).
- GraphQL Server Design: The performance of your AG-Grid largely depends on an efficient GraphQL server. Ensure your resolvers are optimized, use proper database indexing, and handle complex filter/sort/pagination logic effectively.
- Security: Implement authentication and authorization layers in your GraphQL API to control access to data and operations.
Conclusion
Integrating AG-Grid with GraphQL in React provides a robust and scalable solution for managing complex data tables in your applications. By leveraging GraphQL's efficiency for data fetching and AG-Grid's powerful server-side row model, you can deliver a smooth and responsive user experience, even with millions of data points. This combination empowers developers to build highly interactive and performant data dashboards and administrative interfaces.
Experiment with different filter types, sorting options, and pagination settings to fully grasp the flexibility this integration offers. Happy coding!