react-datatable
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()
  }
}

On this page