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
| Option | Type | Description |
|---|---|---|
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
| Property | Type | Description |
|---|---|---|
editingRowId | string | null | Which row is being edited |
draftData | Partial<TData> | Current draft field values |
isDirty | boolean | Whether any field has changed |
dirtyFields | (keyof TData)[] | Which fields changed |
errors | Partial<Record<keyof TData, string>> | Field-level validation errors |
isSaving | boolean | true while onSave promise is in flight |
isEditing | (rowId: string) => boolean | Check if a row is in edit mode |
startEditing | (rowId: string) => void | Enter edit mode (snapshots original) |
setField | <K extends keyof TData>(field: K, value: TData[K]) => void | Update a draft field |
saveEditing | () => Promise<void> | Run onSave, commit or show errors |
cancelEditing | () => void | Discard 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
| Option | Type | Description |
|---|---|---|
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
| Property | Type | Description |
|---|---|---|
editingRowIds | string[] | Rows currently in edit mode |
savingRows | string[] | Rows mid per-row save |
isSavingAll | boolean | true during saveAll() |
hasUnsavedChanges | boolean | true if any row has unsaved changes |
startEditing(id) | fn | Enter edit mode for a row (no-op if already editing) |
setField(id, field, value) | fn | Update a draft field |
saveRow(id) | async fn | Save one row via onSave |
cancelRow(id) | fn | Discard draft and exit edit mode for one row |
saveAll() | async fn | Save all dirty rows |
cancelAll() | fn | Discard all drafts and exit edit mode |
isEditing(id) | fn → boolean | Is this row in edit mode? |
isDirty(id) | fn → boolean | Does 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 → boolean | Is 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, seezodValidator.