#14924·ag-grid

[Bug]: cellRenderer never invoked on initial paint when rowData arrives asynchronously (cells blank until refreshCells)

Author: chaudherysharifCreated Aug 20, 2026Updated Sep 17, 2026
Labelswaiting-for-reprotriage

Link to reproducible scenario

I was not able to attach a Plunker/CodeSandbox link (the template's required field) — creating an account on a third-party sandbox host wasn't an option for me. Instead, below is a complete, self-contained HTML file that reproduces it with no build step and no local install: save it as repro.html, open it in a browser, and read the heading. It loads AG Grid from jsDelivr. Happy to move it into a sandbox if a maintainer prefers.

xml
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ag-grid-community.min.js"></script>
</head>
<body>
  <h3 id="verdict">running…</h3>
  <div id="grid" class="ag-theme-quartz" style="height:250px"></div>
  <button onclick="api.refreshCells({force:true});report()">refreshCells({force:true})</button>
<script>
let rendererCalls = 0;
agGrid.ModuleRegistry.registerModules([agGrid.AllCommunityModule]);

const api = agGrid.createGrid(document.getElementById('grid'), {
  rowModelType: 'clientSide',
  columnDefs: [
    { field: 'plain' },
    { field: 'custom', cellRenderer: (p) => { rendererCalls++; return 'rendered:' + p.value; } },
  ],
  onGridReady: (p) => {
    // Data arrives asynchronously, as it would from a real request.
    setTimeout(() => p.api.setGridOption('rowData',
      Array.from({length:10}, (_,i) => ({ plain:'row '+i, custom:'value '+i }))), 200);
  },
});

function report() {
  const cells = [...document.querySelectorAll('.ag-cell')].filter(c => c.getAttribute('col-id')==='custom');
  document.getElementById('verdict').textContent =
    `rendererCalls=${rendererCalls}  |  custom cells=${cells.length}  blank=${cells.filter(c=>!c.textContent.trim()).length}`;
}
setTimeout(report, 2000);
</script>
</body>
</html>

Describe the bug

Steps to reproduce

  1. Open the file above.
  2. Read the heading after it settles (~2s).
  3. Click refreshCells({force:true}) and read the heading again.

Actual behaviour

rendererCalls=0   | custom cells=10  blank=10     <- initial paint
rendererCalls=10  | custom cells=10  blank=0      <- after refreshCells({force:true})

The cellRenderer is never invoked for the initial render of rows that arrive asynchronously. The column renders as completely empty cells. The plain column beside it renders normally, so rows and values are present — only the custom-rendered column is blank. api.getCellRendererInstances({columns:['custom']}) returns [] during this window.

There is no exception and no console warning, which makes it easy to ship: a date column just looks empty, and an action column renders no button at all, so the feature reads as missing rather than broken.

Any forced refresh afterwards fixes it permanently — the same renderer then runs correctly, so the renderer itself is fine; it is only skipped on the first paint.

Expected behaviour

The cellRenderer is invoked for the rows delivered by the first rowData / first Server-Side block, and the cells render without needing a manual refreshCells.

More information

Scope, from testing each variable independently rather than assuming:

  • Both row models. clientSide (above) and serverSide — with SSRM, neither firstDataRendered nor storeRefreshed ever fires at all, so there is no natural lifecycle hook to hang a workaround on; modelUpdated is the only event that fires once real data has landed.
  • Both renderer shapes — a plain function (above) and an ICellRendererComp class.
  • Community and Enterprise — reproduced on ag-grid-community (no licence involved) and on ag-grid-enterprise, so it is not a licensing/trial-path artefact.
  • Not a module-registration problem — reproduced with the full AllCommunityModule registered, and with an explicit module list; all module identifiers verified present on the global before registering.
  • Not new in 36.x — also reproduces on [email protected].
  • Timing matters: the repro delays setRowData by 200ms to mimic a real async fetch. Setting row data synchronously in the same tick does not show it.

Guess at why this may be under-reported: it seems to need the vanilla-JS createGrid path plus asynchronously-arriving data. The framework wrappers may not hit it, and a synchronous demo dataset does not.

Workaround we are shipping, in case it helps anyone finding this issue: hook modelUpdated once and call api.refreshCells({ force: true, suppressFlash: true }). force: true is required — an unforced refresh is a no-op because the underlying value never changed. It does not re-raise modelUpdated, so there is no refresh loop.

Version

36.1.0 (also reproduced on 36.0.2 and 35.3.1)

Does the issue occur for a specific framework only?

JavaScript

Is the issue only observable on a specific browser?

No — observed on Chromium.