Scientyfic World

Write Structured content faster — Get your copy of DITA Decoded today. Buy it Now

How to Integrate React Query with AG Grid?

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>
  );
}
JavaScript

In 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>
  );
}
JavaScript

Here, 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]);
JavaScript

This 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-populated rowData will update automatically.
  • For server-side grids, call the grid API (e.g. gridApi.refreshServerSideStore()) to re-run getRows, 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 in useQuery and in fetchQuery. 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 any useQuery calls) outside of any callbacks. Do not call hooks inside getRows or onGridReady – those are plain functions. React’s hook rules forbid calling hooks from regular functions.
  • Memoize datasources. If you use useMemo or useCallback to create your serverSideDatasource, list queryClient and any relevant props (like workspaceId) 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 new getRows call. In client-side mode, simply relying on useQuery 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.

Snehasish Konger
Snehasish Konger

Snehasish Konger is a passionate technical writer and the founder of Scientyfic World, a platform dedicated to sharing knowledge on science, technology, and web development. With expertise in React.js, Firebase, and SEO, he creates user-friendly content to help students and tech enthusiasts. Snehasish is also the author of books like DITA Decoded and Mastering the Art of Technical Writing, reflecting his commitment to making complex topics accessible to all.

Articles: 230