I have a React app using AG-Grid that looks like this:
User interaction: The user selects a couple filter values from the dropdowns and clicks 'Get Data'. The app then sends a request to the backend, which sends a request to an external API to retrieve data based on the chosen filter values. The grid is then populated on the frontend.
Question: How can I efficiently automatically resize the columns (to fit the content within them) every time I re-render the grid with new data (i.e. every time the 'get data' button is pressed)?
My Code for Grid.jsx
import { AgGridReact } from 'ag-grid-react';
import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
import 'ag-grid-community/styles/ag-grid.css'; // Core grid CSS, always needed
import 'ag-grid-community/styles/ag-theme-alpine.css'; // Optional theme CSS, comment out for custom styling
import './Grid.css';
const Grid = ({ gridData }) => {
const gridRef = useRef();
// Columns definition
const [columnDefs, setColumnDefs] = useState([]);
// Data/rows definition
const [rowData, setRowData] = useState([]);
// Transform grid data into format ag-grid can use
const transformGridData = () => {
const { columns, rows, cells } = gridData;
const transformedColumnDefs = [];
const transformedRowData = [];
// ...
// (omitted) A BUNCH OF LOGIC FOR PARSING THE DATA AND CONVERTING IT INTO AG-GRID's COLS AND ROWS
// ...
// Set column defs and row data
setColumnDefs(transformedColumnDefs);
setRowData(transformedRowData);
}
// Default column style/functionality definition
const defaultColDef = useMemo(() => ({
// default col settings
}), []);
// Update the grid when data given by external API changes
useEffect(() => {
if (gridData != null) {
transformGridData();
}
}, [gridData]);
return (
<div className="ag-theme-alpine grid-div" style={{ height: 600, marginTop: 50, marginLeft: 17 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
/>
</div>
)
}
export default Grid;
What I've tried:
const autoSizeAll = useCallback(() => {
const allColumnIds = [];
gridRef.current.columnApi.getColumns().forEach((column) => {
allColumnIds.push(column.colId);
});
gridRef.current.columnApi.autoSizeColumns(allColumnIds);
}, []);
return (
<div className="ag-theme-alpine grid-div" style={{ height: 600, marginTop: 50, marginLeft: 17 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onFirstDataRendered={autoSizeAll}
onDisplayedColumnsChanged={autoSizeAll}
suppressColumnVirtualization={true}
/>
</div>
)
}
onFirstDataRendered
and onDisplayedColumnsChanged
and instead manually calling autoSizeAll()
immediately after calling setColumnDefs(transformedColumnDefs)
and setRowData(transformedRowData)
in transformGridData()
, like so: ...
// Set column defs and row data
setColumnDefs(transformedColumnDefs);
setRowData(transformedRowData);
autoSizeAll();
}
This resulted in some very odd behavior. Clicking 'get data' once would populate the grid, but not autosize the columns. Clicking it again, however, would properly autosize the columns. This works the same way if I change the chosen filter values. First click populates, the second click autosizes.
Is there a solution to this problem that would work efficiently?
The setTimeout()
solution suggested by Ahmet worked, but it felt a bit risky relying on an arbitrary timeout. I also did some research and found there was no real way to check if an AG-grid component was done rendering (besides onFirstDataRendered
). Additionally, I also noticed a small "shifting" effect where the grid would be displayed on screen, and a split-second later all the columns would shift as they autosized to fit their data. This is of course because the autosizing would occur after the columns were rendered.
This led me to try manually autosizing columns, which has been working much better so far.
I have a measureTextWidth
function, which determines the pixel length of a string based on my defined fontSize
and fontFamily
variables:
// Measures the pixel width of a string
const measureTextWidth = (text) => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${fontSize} ${fontFamily}`;
return context.measureText(text).width;
};
As I build my rows and columns (in temporary variables), I update a dictionary called maxLengths
that stores column labels as keys and the lengths of their longest cell content as values.
In my case, that looks something like this:
// Update max content length for this column (for autosizing)
maxLengths[cell.formattedColCoordinates] = Math.max(
maxLengths[cell.formattedColCoordinates] || 0,
measureTextWidth(cell.formattedValue)
);
Finally, I update all the column widths before giving them and the row data to columnDefs
and rowData
:
// Autosize: Set column widths according to max content length
transformedColumnDefs.forEach((columnDef) => {
columnDef.width = maxLengths[columnDef.field] + cellPadding;
});
// Set column defs and row data
setColumnDefs(transformedColumnDefs);
setRowData(transformedRowData);
This approach has led to consistent, smooth and fast autosizing (no need to set suppressColumnVirtualization={true}
and reduce my grid performance).