Tablecraft
API Reference

Editing

useEditableRows for single-row inline editing, and useMultiRowEditing for editing and saving multiple rows at once.

Both hooks are standalone opt-in hooks — no form library required. They work with Zod, Yup, or plain validation via a shared error-map contract: onSave returns void (or undefined) to commit, or a Partial<Record<keyof TData, string>> of field errors to stay in edit mode.

useEditableRows

Single-row inline editing.

function useEditableRows<TData extends RowData>(
  table: Table<TData>,
  options?: EditableOptions<TData>
): EditableReturn<TData>

Options

OptionTypeDescription
onSave(rowId: string, draft: TData) => void | Partial<Record<keyof TData, string>> | Promise<...>Called on save. Return an error map to stay in edit mode, or nothing to commit.

Return

PropertyTypeDescription
editingRowIdstring | nullWhich row is being edited
draftDataPartial<TData>Current draft field values
isDirtybooleanWhether any field has changed
dirtyFields(keyof TData)[]Which fields changed
errorsPartial<Record<keyof TData, string>>Field-level validation errors
isSavingbooleantrue while onSave promise is in flight
isEditing(rowId: string) => booleanCheck if a row is in edit mode
startEditing(rowId: string) => voidEnter edit mode (snapshots original)
setField<K extends keyof TData>(field: K, value: TData[K]) => voidUpdate a draft field
saveEditing() => Promise<void>Run onSave, commit or show errors
cancelEditing() => voidDiscard changes, exit edit mode

Usage

import { useTable, useEditableRows } from '@marvinackerman/tablecraft'

const { table } = useTable({ data, columns })
const editable = useEditableRows(table, {
  onSave: async (rowId, draft) => {
    // Return an error map to show validation errors and stay in edit mode
    if (!draft.name) return { name: 'Name is required' }

    // Or use any schema library
    const result = schema.safeParse(draft)
    if (!result.success) return formatErrors(result.error)

    // Return nothing (or undefined) to commit and exit edit mode
    await api.updateUser(rowId, draft)
  },
})

// In your row render:
{table.getRowModel().rows.map((row) => (
  <tr key={row.id}>
    {editable.isEditing(row.id) ? (
      <>
        <td>
          <input
            value={editable.draftData.name ?? ''}
            onChange={(e) => editable.setField('name', e.target.value)}
          />
          {editable.errors.name && <span>{editable.errors.name}</span>}
        </td>
        <td>
          <button onClick={editable.saveEditing}>Save</button>
          <button onClick={editable.cancelEditing}>Cancel</button>
        </td>
      </>
    ) : (
      <>
        <td>{row.original.name}</td>
        <td>
          <button onClick={() => editable.startEditing(row.id)}>Edit</button>
        </td>
      </>
    )}
  </tr>
))}

useMultiRowEditing

Edit, validate, and save multiple rows simultaneously — per-row save and bulk save are both supported.

function useMultiRowEditing<TData extends RowData>(
  table: Table<TData>,
  options?: MultiRowEditingOptions<TData>
): MultiRowEditingReturn<TData>

Options

OptionTypeDescription
onSave(rowId, draft) => void | errors | Promise<...>Per-row save callback. Return a non-empty errors object to stay in edit mode.
onSaveAll(rows: { rowId, draft }[]) => void | Record<rowId, errors> | Promise<...>Batch save callback. If omitted, saveAll() calls onSave for each dirty row in parallel.

Return

PropertyTypeDescription
editingRowIdsstring[]Rows currently in edit mode
savingRowsstring[]Rows mid per-row save
isSavingAllbooleantrue during saveAll()
hasUnsavedChangesbooleantrue if any row has unsaved changes
startEditing(id)fnEnter edit mode for a row (no-op if already editing)
setField(id, field, value)fnUpdate a draft field
saveRow(id)async fnSave one row via onSave
cancelRow(id)fnDiscard draft and exit edit mode for one row
saveAll()async fnSave all dirty rows
cancelAll()fnDiscard all drafts and exit edit mode
isEditing(id)fn → booleanIs this row in edit mode?
isDirty(id)fn → booleanDoes this row have unsaved changes?
dirtyFields(id)fn → (keyof TData)[]Which fields have changed?
getDraft(id)fn → Partial<TData>Current draft for a row
getErrors(id)fn → Partial<Record<keyof TData, string>>Current field errors for a row
isSavingRow(id)fn → booleanIs this row currently being saved?

Usage

import { useTable, useMultiRowEditing } from '@marvinackerman/tablecraft'

const { table } = useTable({ data, columns })

const {
  editingRowIds,
  startEditing,
  setField,
  getDraft,
  getErrors,
  saveRow,
  cancelRow,
  saveAll,
  cancelAll,
  isEditing,
  isDirty,
  hasUnsavedChanges,
  isSavingAll,
} = useMultiRowEditing(table, {
  onSave: async (rowId, draft) => {
    const errors = await api.updateUser(draft)
    if (errors) return errors          // { name: 'Too long' } keeps row in edit mode
    // return void / undefined → row exits edit mode
  },
  onSaveAll: async (rows) => {
    const result = await api.bulkUpdate(rows)
    // Return Record<rowId, errors> — rows with errors stay in edit mode
    return result.errors
  },
})

{hasUnsavedChanges && (
  <button onClick={saveAll} disabled={isSavingAll}>
    {isSavingAll ? 'Saving…' : 'Save All'}
  </button>
)}

Notes

  • For a schema-driven validator that plugs straight into either hook's onSave, see zodValidator.

On this page