React enforces the Rules of Hooks: you cannot call hooks (like useQuery
) inside loops, conditions or arbitrary callbacks. Hooks must run at the top level of a React component or custom Hook. AG Grid’s callbacks (like onGridReady
or getRows
) are plain functions invoked by the grid, not React render functions. Therefore, you should never call useQuery
inside them – doing so breaks the hook rules. Instead, obtain the React Query client via const queryClient = useQueryClient()
in your component, and use its imperative API (queryClient.fetchQuery
) inside those callbacks. The fetchQuery
method will fetch and cache data outside of React’s render cycle, preserving React Query’s cache, retry and invalidation behaviour.
What is AG Grid?
AG Grid is a high-performance data grid library used in React applications to display and manage table data. It supports rich features like sorting, filtering, pagination, and editing. AG Grid offers multiple row models to control how data is loaded:
- Client-Side Row Model: The default mode. All data is loaded into the grid at once, and filtering/sorting/grouping are done in the browser’s memory. Use this when the full dataset is not too large.
- Server-Side Row Model: Used for very large or hierarchical datasets. The grid loads data on demand (e.g. as the user scrolls or expands groups) by calling a data source that fetches pages of data from the server. In this model, filtering and sorting typically happen on the server.
Depending on data size and use case, these models let you choose between simple client-side handling (load all rows) or server-driven lazy loading (fetch pages).
The problem statement
When integrating AG Grid with React Query, a common challenge arises: AG Grid’s callbacks (like onGridReady
or the getRows
function in a Server-Side datasource) are not React components, so you cannot call React hooks such as useQuery
inside them. Doing so violates React’s Rules of Hooks, which require that hooks only be called at the top level of a React component or custom hook. For example, trying to call useQuery
inside onGridReady
or getRows
will either be disallowed by linting or lead to incorrect behavior.
Instead, the solution is to use React Query’s QueryClient
API imperatively in those callbacks. Specifically, we obtain the queryClient
(via useQueryClient()
) and call queryClient.fetchQuery({ queryKey, queryFn })
inside the AG Grid callbacks to fetch and cache data. This fetches data and updates the React Query cache without using a hook. After fetching, we pass the data back to AG Grid (for example by calling params.successCallback(rowData)
). When data needs to be refreshed (e.g. after a mutation or a manual refresh action), we call queryClient.invalidateQueries(queryKey)
to mark the query stale and trigger React Query to refetch.
In this blog we’ll apply this pattern to both AG Grid modes: for the client-side row model (loading all data on grid ready) and for the server-side row model (loading pages via getRows
). By using queryClient.fetchQuery
and queryClient.invalidateQueries
rather than useQuery
inside AG Grid callbacks, we preserve caching and invalidation while adhering to Hooks rules.
Client-side Row Model
With AG Grid’s default client-side model, simply load all needed data via useQuery
in your component and pass it to the grid. For example:
function SpacesTable({ workspaceId }) {
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery(
['spaces', workspaceId],
() => fetchSpacesByWorkspaceId(workspaceId)
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return (
<div>
<button onClick={() => queryClient.invalidateQueries(['spaces', workspaceId])}>
Refresh Data
</button>
<AgGridReact
columnDefs={colDefs}
defaultColDef={defaultColDef}
// Provide the queried data array to the grid
rowData={data.spaces}
/>
</div>
);
}
JavaScriptIn this setup, React Query handles fetching/caching. The grid renders whatever is in data.spaces
. To reload the data, call queryClient.invalidateQueries(['spaces', workspaceId])
; React Query will mark the query stale and automatically refetch. When the hook returns new data, the grid will update its rows. No grid-specific callbacks (onGridReady
or getRows
) are needed for data loading in client-side mode.
Server-side Row Model
For AG Grid’s server-side model (e.g. infinite scrolling or pagination), you must implement a serverSideDatasource
with a getRows(params)
function. Inside getRows
, do not use useQuery
(a hook). Instead, use the already-obtained queryClient
to fetch data. For example:
function SpacesTable({ workspaceId }) {
const queryClient = useQueryClient();
const [gridApi, setGridApi] = useState(null);
// Define the datasource once (memoize to avoid recreation)
const dataSource = useMemo(() => ({
getRows: async (params) => {
const { startRow, endRow, sortModel, filterModel } = params.request;
// Compose a unique query key that includes paging/sort/filter info
const queryKey = [
'spaces', workspaceId, startRow, endRow, sortModel, filterModel
];
// Fetch via React Query (will use cache if not invalidated)
const result = await queryClient.fetchQuery(queryKey, () =>
fetchSpacesByWorkspaceId(workspaceId, startRow + 1, endRow, sortModel, filterModel)
);
// Supply the fetched data to the grid
if (result) {
params.success({
rowData: result.spaces,
rowCount: result.total,
});
} else {
params.fail();
}
}
}), [workspaceId, queryClient]);
const onGridReady = (params) => {
setGridApi(params.api);
params.api.setServerSideDatasource(dataSource);
};
return (
<div>
<button onClick={() => {
// Invalidate and refresh via grid API (see below)
queryClient.invalidateQueries(['spaces', workspaceId]);
gridApi?.refreshServerSideStore({ purge: true });
}}>
Refresh Table
</button>
<AgGridReact
onGridReady={onGridReady}
rowModelType="serverSide"
columnDefs={colDefs}
defaultColDef={defaultColDef}
// no need to pass rowData for server-side mode
/>
</div>
);
}
JavaScriptHere, we call queryClient.fetchQuery(queryKey, queryFn)
inside getRows
. This imperatively fetches the data (and caches it under queryKey
) without using a hook. Once the promise resolves, we call params.success({ rowData, rowCount })
to give AG Grid the data. Note that we include paging, sorting, and filtering in the queryKey
so each distinct request caches separately. AG Grid’s server-side row model will call this getRows
on scroll or sort events.
Invalidation and Refresh
To refresh the data, use React Query’s invalidation API. For example:
// In an event handler (e.g. button onClick):
queryClient.invalidateQueries(['spaces', workspaceId]);
JavaScriptThis marks any active queries with that key as stale and triggers background refetch. In client-side mode, the useQuery
hook will refetch and update the grid data automatically. In server-side mode, after invalidation you should tell AG Grid to reload data by calling its refresh API (e.g. gridApi.refreshServerSideStore({ purge: true })
or refreshServerSideStore()
in newer versions). This causes AG Grid to call getRows
again, and since the query was invalidated, fetchQuery
will fetch fresh data. In summary:
- Call
queryClient.invalidateQueries(['spaces', workspaceId])
to mark cached data stale. - For client-side grids, the
useQuery
-populatedrowData
will update automatically. - For server-side grids, call the grid API (e.g.
gridApi.refreshServerSideStore()
) to re-rungetRows
, which will use the (now stale) query key to fetch new data.
Best Practices
- Define queries in one place. Write your data-fetching logic (e.g.
fetchSpacesByWorkspaceId
) once and use it both inuseQuery
and infetchQuery
. Keep the query key consistent (e.g.['spaces', workspaceId]
) so React Query can manage caching properly. This avoids duplicating logic. - Call hooks only at the top level. In your component, call
const queryClient = useQueryClient()
(or anyuseQuery
calls) outside of any callbacks. Do not call hooks insidegetRows
oronGridReady
– those are plain functions. React’s hook rules forbid calling hooks from regular functions. - Memoize datasources. If you use
useMemo
oruseCallback
to create yourserverSideDatasource
, listqueryClient
and any relevant props (likeworkspaceId
) in the dependency array. This prevents recreating the function on every render. - Stable query keys. Include all request parameters (page range, filters, etc.) in the query key so each fetch is cached under a unique key. For example:
['spaces', workspaceId, startRow, endRow, sortModel, filterModel]
. - Refresh via grid API. After invalidation, use AG Grid’s API (
refreshServerSideStore
or similar) to force a newgetRows
call. In client-side mode, simply relying onuseQuery
refetch is enough.
By separating data fetching (React Query) from the grid callbacks (which only use the imperative API), you preserve React Query’s powerful features (caching, retry, automatic refetching) without breaking the Rules of Hooks. The above approach lets you invalidate and refresh table data via React Query cleanly in both client- and server-side row models.
Conclusion
In summary, the solution is to keep React Query hooks at the component level and use the QueryClient directly inside AG Grid callbacks. We never call useQuery
in getRows
or onGridReady
; instead we fetch data with queryClient.fetchQuery({queryKey, queryFn})
imperatively. We then supply the data to AG Grid (e.g. via state or successCallback
) and use queryClient.invalidateQueries(queryKey)
when we need to refresh or refetch. This pattern cleanly separates concerns: AG Grid handles rendering and invokes callbacks, while React Query manages data fetching, caching, and refetching. By doing this, we maintain React Query’s full functionality (caching, background updates) without breaking the Rules of Hooks.