Examples
Editable text cell
Use this when a row needs lightweight inline text edits without turning the table into a spreadsheet.
Interactive cell
Live preview
Build the React component
The React component is the whole story here: switch between read and edit mode, keep a local draft, and commit on blur or Enter.
import { useEffect, useState } from "react"
import { Input } from "@/components/ui/input"
export function EditableCell({
value,
onChange,
}: {
value: string
onChange: (value: string) => void
}) {
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(value)
useEffect(() => {
setDraft(value)
}, [value])
if (!editing) {
return (
<button
className="w-full rounded-md px-1.5 py-1 text-left text-sm hover:bg-accent"
onClick={() => setEditing(true)}
type="button"
>
<span className="block truncate">{value}</span>
</button>
)
}
return (
<Input
autoFocus
className="h-8 border-none bg-transparent px-1.5 text-sm shadow-none ring-1 ring-ring"
onBlur={() => {
onChange(draft.trim() || value)
setEditing(false)
}}
onChange={(event) => setDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
onChange(draft.trim() || value)
setEditing(false)
}
if (event.key === "Escape") {
setDraft(value)
setEditing(false)
}
}}
value={draft}
/>
)
}Wire it into a column definition
The column keeps a stable text field for the table. The cell only manages the local editing surface.
Even with a custom cell, the column still needs an accessor. In this example that is
accessorKey="notes". That gives the table a stable field for sorting, filtering, and saved state, while the cell can still read extra data from row.original.{
id: "notes",
header: "Notes",
accessorKey: "notes",
width: 240,
enableSorting: false,
enableFiltering: false,
cell: ({ row }) => (
<EditableCell
value={row.original.notes}
onChange={(notes) => updateRow(row.original.id, { notes })}
/>
),
}Render it in a small table
The mini table shows the inline editor in context without stacking multiple editable cell types into the same demo.
function NotesExampleTable() {
const [rows, setRows] = useState(seedRows)
const updateRow = (id: string, patch: Partial<CustomerRow>) => {
setRows((current) => current.map((row) => (row.id === id ? { ...row, ...patch } : row)))
}
return <Datatable tableKey="custom-cells" data={rows} columns={columns(updateRow)} getRowId={(row) => row.id} toolbar={false} />
}Mini table demo
Loading table preferences...
Persist changes with optimistic update
For backend sync, keep the optimistic patch small and let refetch clean up any validation or canonical formatting differences.
async function updateNotes(id: string, notes: string) {
setRows((current) => current.map((row) => (row.id === id ? { ...row, notes } : row)))
try {
await api.customers.updateNotes({ id, notes })
await refetchCustomers()
} catch {
await refetchCustomers()
}
}