Home/Cells & Formatting/Value Getters & Formatters
Cells & Formatting

Value Getters & Formatters

Three hooks decide what a cell holds, how it reads, and how an edit is written back. The rule behind most of the bugs: a formatter changes the text and nothing else, so sorting, column filters and editing keep working on the real value.

Value getters

A column with no field of its own. Full Name is built from two fields, Years is computed — and both sort and filter like real ones.

#
Full Name
Department
Years
1
Omar Smith
Engineering
9
2
Carla Haddad
Marketing
8
3
Petra Novak
Support
7
4
Ben Okafor
Legal
6
5
Nadia Smith
Operations
5
6
Idris Haddad
Engineering
4
7
Alice Novak
Marketing
3
8
Lena Okafor
Support
2
9
Farid Smith
Legal
1
10
Greta Haddad
Operations
0
11
Dmitri Novak
Engineering
9
12
Karim Okafor
Marketing
8
13
Hana Smith
Support
7
14
Elena Haddad
Legal
6
15
Mo Novak
Operations
5
16
Jo Okafor
Engineering
4
All records loaded
16 / 16 records

When you need one

Reach for valueGetter when the column has no field to point at. Full Name is built from two fields; Years is arithmetic on a third. Neither exists on the row, and neither needs to.

{
  key: 'fullName', name: 'Full Name',
  valueGetter: row => `${row.firstName} ${row.lastName}`,
  sortable: true, filter: { type: 'text' },
}

A getter replaces prop. Set both and the getter wins, so delete the prop rather than leaving it there to be read later as if it mattered.

It is a real column

Because the getter produces the value itself, everything downstream sees it. Sort Full Name and the rows order by "Alice Smith". Type into its filter and the search runs over the same text. Sort Years and the order is numeric, because the getter returned a number and nothing has turned it into a string yet.

Value formatters

The same value, written for a human. Sort Salary, edit it, then type Jan 2016 into the Started filter and again into the search box — one grid walks the whole formatted-versus-raw rule.

#
Salary
Started
Years
1
$40,000
Jan 2016
9 yr
2
$46,500
Feb 2017
8 yr
3
$53,000
Mar 2018
7 yr
4
$59,500
Apr 2019
6 yr
5
$66,000
May 2020
5 yr
6
$72,500
Jun 2021
4 yr
7
$79,000
Jul 2022
3 yr
8
$85,500
Aug 2023
2 yr
9
$92,000
Sep 2024
1 yr
10
$98,500
Jan 2025
under a year
11
$105,000
Feb 2016
9 yr
12
$111,500
Mar 2017
8 yr
13
$118,000
Apr 2018
7 yr
14
$124,500
May 2019
6 yr
15
$131,000
Jun 2020
5 yr
16
$137,500
Jul 2021
4 yr
All records loaded
16 / 16 records

Display only — and that is the point

Salary reads as $92,000. Sort it: the order is numeric, not alphabetical. Its filter is a number box, so you type 92000 — the value, never the text. Double-click a cell: the editor opens on 92000 too.

That is the whole rule in one column. A grid that formatted before it sorted would put $1,000 before $90, which is the classic version of this bug.

Two boxes, one column, two different answers

Started shows Jan 2016 and stores 2016-01-10. Both boxes below take text, so nothing but the value they read separates them:

  • Type Jan 2016 into the column filter under Started — no rows. It compares against the stored date.
  • Type Jan 2016 into the quick-filter box above the grid — the rows appear. It searches what is on screen.

That is deliberate rather than an inconsistency: someone typing into a search box is copying what they can see, while a column filter is a comparison against the real value. Type 2016-01 into the column filter and it matches.

Keep them pure and cheap

A formatter runs for every visible cell on every render. Build the Intl.NumberFormat once outside the column definition, not inside the function. And do not read anything that can change underneath it — a formatter that reaches for the clock will disagree with itself between two renders. Years above is measured against a fixed date for exactly that reason.

A formatter that throws does not take the grid down: the cell falls back to the raw value, and one warning is logged for the column rather than one per cell.

Value setters

Writing an edit back to a nested field. Email lives at contact.email; Department does not, and needs no setter at all.

#
Department
Email
1
Engineering
omar.smith@example.com
2
Marketing
carla.haddad@example.com
3
Support
petra.novak@example.com
4
Legal
ben.okafor@example.com
5
Operations
nadia.smith@example.com
6
Engineering
idris.haddad@example.com
7
Marketing
alice.novak@example.com
8
Support
lena.okafor@example.com
9
Legal
farid.smith@example.com
10
Operations
greta.haddad@example.com
11
Engineering
dmitri.novak@example.com
12
Marketing
karim.okafor@example.com
13
Support
hana.smith@example.com
14
Legal
elena.haddad@example.com
15
Operations
mo.novak@example.com
16
Engineering
jo.okafor@example.com
All records loaded
16 / 16 records

Last committed edit:Edit an email cell — the write goes through this column's valueSetter.

Reading is free, writing is not

prop: 'contact.email' reads the nested field with no code at all — the cells above are proof. Writing is where it breaks. The default write is a shallow set on prop, so committing an edit would put the string where the contact object used to be.

valueSetter: (row, value) => ({
  ...row,
  contact: { ...row.contact, email: String(value) },
})

The setter returns a new row. That is what keeps the change visible to everything watching, and it is why the row is spread rather than mutated.

Most columns need no setter

Department above is editable and has no valueSetter. It does not need one: the field sits at the top level, so the default shallow write is already correct. Add a setter when the default would be wrong — which in practice means anything nested, or a value that has to be parsed before it is stored.

cellEditCommit hands you the row the setter returned, so the object you send to your API is the one the grid is showing.

Three hooks, three different jobs

A cell's text is produced in stages, and each stage has its own hook. Knowing which one to reach for is most of the subject:

  • valueGetter — what the value is. Use it when a column has no field of its own: a combination of two fields, something computed, something nested. It replaces prop.
  • valueFormatter — how the value is printed. It changes the text and nothing else; the underlying value is untouched.
  • valueSetter — how an edit is written back. Use it when the default write would be wrong, which is mainly anything nested.

So the question to ask is: is this a different value, or the same value written differently? Different value → getter. Same value → formatter.

The rule that prevents the bugs: formatting is display-only

A formatted column still sorts, filters and edits as its raw value. What sees the formatted text is the reader — and the two features whose job is to match what the reader can see:

Uses the formatted textUses the raw value
the cell itselfsorting
the quick-filter boxcolumn filters
an exported CSV fileediting — the editor opens on the raw value

The second example below walks every row of that table on one grid. If you need a file to carry something other than the on-screen text, decide it in the export rather than in the column — see What Gets Exported.

Sorting a computed column

The examples here use sortMode: 'client', so the grid sorts the rows it has loaded using the getters. With the default server-side sorting your endpoint decides the order, and it has never heard of a column called Full Name — so either translate the sort key on the way out, or put the column in client mode. It is worth choosing deliberately: a computed column with a server-side sort that silently does nothing is easy to ship.

filterApplyMode: 'client' works the same way and carries the same caveat: it filters the rows the grid has, so pair it with scrollMode: 'infinite' rather than with paging.

A valueGetter receives the column as its second argument, so one function can serve several columns — read column.key and branch, instead of writing near-identical getters.

Value pipeline options

PropertyTypeDefaultDescription
column.valueGetter(row, column) => any—Produces the column's value. Replaces prop, and everything downstream — sorting, filtering, editing — uses what it returns.
column.valueFormatter(value, row, column) => string—Turns the resolved value into the text shown in the cell. Display only: sorting, column filters and editing keep using the raw value. A throwing formatter falls back to the raw value and warns once per column.
column.valueSetter(row, value) => Tshallow set on <code>prop</code>Writes a committed edit back and returns a new row. Needed for nested fields, where the default write would replace the parent object.
column.propkeyof T | string—The field to read, including a dot path such as contact.email. Ignored when a valueGetter is present.
config.sortMode'server' | 'client''server'Where sorting happens. 'client' sorts loaded rows with the columns' getters — which is what a computed column needs.
config.filterApplyMode'server' | 'client''server'Where filtering happens. 'client' filters the loaded rows against each column's resolved value, and requires scrollMode: 'infinite'.
config.quickFilterboolean | QuickFilterConfigfalseThe search box that matches the formatted on-screen text across every column.
column.editablebooleanfalseLets a cell be edited; the committed value goes through valueSetter. See Cell Editing.