By Brendan Woodell
Recently, one of our applications that heavily uses Ag-Grid needed to be refactored using a Server-Side Row Model. Originally we could preload all relevant data into the browser and interact with it through Ag-Grid. However, this dataset became too large to hold in memory and neccesitated a big refactor effort. What that entails is an API method for virtually every interaction, fetching 100 rows at a time, sorted and filtered on the server, and displaying those. Scrolling the grid will fetch the next 100 rows. Changing a sort or filter will clear the rows and fetch the first 100 rows of this new dataset.
This refactoring effort, while largely successful and very cool in its implementation, created a lot of hurdles for us to recreate certain features that were provided for free in the Preloaded Data Model.
One of those features was automatically updating our dropdown filters, also called Set Filters (as in a Set of checkbox filter options), based on changing filters within the grid. We nicknamed this functionality as “Cascading Filters”, by cascading filter changes to other column filters within the grid.
If a Continent column is filtered to Europe, then a Country column dropdown filter would only show Countries in Europe available in the filter set.
This is an easy feature for Ag-Grid to provide when all of the data is available in the browser. In a Server-Side Row Model, replicating this behavior requires additional API methods to fetch distinct filter options from the dataset on the server, applying existing filters, every time a filter dropdown is opened.
Ag-Grid provides the ability to asynchronously fetch filter values by providing a function in the filterParams
object, and instructions to refetch values each time the filter is opened with the refreshValuesOnOpen
parameter. You might decide to pass the current grid filters to this function and update the function definition in a useCallback
hook.
const FetchValuesAsynchronously = useDeepCompareCallback(async (colDef) => {
return await fetch("/api/getFilters",
{
method: "GET",
body: JSON.stringify({columnName: coldef.field, filters: apiFilterModel})
});
}, [apiFilterModel]}
const columnDefs = {
...
filterParams: {
refreshValuesOnOpen = true,
values: async (params: SetFilterValuesFuncParams): void => {
params.success(await FetchValuesAsynchronously(params.colDef))
},
},
...
}
return (
<AgGridReact
...
columnDefs={columnDefsWithFilters}
...
/>
)
However, this becomes a closure in Ag-Grid. The values
function may be reevaluated each time, but the FetchValuesAsynchronously
function defintion never is, at least not by Ag-Grid. Due to the React rendering cycle, we can see that both the component’s knowledge of columnDefs
and the function definition of FetchValuesAsynchronously
should be updated whenever the grid component rerenders. Unfortunately, our experience was that FetchValuesAsynchronously
was not refreshed when we did this. Primitive values, like refreshValuesOnOpen
for example, could be changed and updated in the grid this way; but more deeply nested or complex objects like a function would be held and not updated.
This made providing Cascading Filter functionality difficult and left us unsure what value an asynchronous filterParams.values
method provided. So how did we solve this?
Refs!
The answer felt obvious once we saw it in action. How does one update something that cannot be updated? Provide a reference to something that can! The reference is like a phone number, FetchValuesAsynchronously()
is more akin to details about the phone. It doesn’t matter how those details change, the phone number will always go to the same phone. In practice, that looks more like:
const fetchValuesRef = useRef();
const FetchValuesAsynchronously = useDeepCompareCallback(async (colDef) => {
return await fetch("/api/getFilters",
{
method: "GET",
body: JSON.stringify({columnName: coldef.field, filters: apiFilterModel})
});
}, [apiFilterModel]}
useDeepCompareEffect(() => {
fetchValuesRef.current = FetchValuesAsynchronously;
}, [apiFilterModel])
const columnDefs = {
...
filterParams: {
refreshValuesOnOpen = true,
values: async (params: SetFilterValuesFuncParams): void => {
params.success(await fetchValuesRef.current(params.colDef))
},
},
...
}
return (
<AgGridReact
...
columnDefs={columnDefsWithFilters}
...
/>
)
This approach works beautifully! Ag-Grid can hold onto the ref, since the ref will never change. The function that the ref points to, however, can change as often as we need it to. With that approach, we’re able to provide our customers with the experience they’re used to in a model that allows the application to continue functioning with a massive dataset.