---
name: data-grid
description: "Use when: building or configuring MUI X DataGrid tables, editable columns, toolbar actions, row selection, or TanStack DB-backed grids."
---

## Rules

- Wrap DataGrid in `Fullscreen` when it is the only content on the page.
- Type columns as `Array<GridColDef<RowType>>` with the row interface.
- Use translation for all `headerName` values.
- Use `rows`, `columns`, `showToolbar`, `loading`, `ignoreDiacritics`, `cellSelection`, and `disableRowSelectionOnClick` on grids.
- For enums use `type: "singleSelect"` with `{ value, label }` options array used from enumObject.
- For editable columns, pair `editable: true` with `renderHeader: EditableColumnHeader`.
- Use `ChipInputCell`, `EditableColumnHeader`, and router button/grid action components from `wcz-layout/components` when they fit.

## File Placement

```
@mui/x-data-grid-premium        — GridColDef, DataGridPremium
src/db-collections/             - TanStack DB collections to get data
@tanstack/react-db              - useLiveQuery, useLiveSuspenseQuery
src/server/actions/             - server functions and enums
wcz-layout/components           - ChipInputCell, EditableColumnHeader, Fullscreen
wcz-layout/hooks                - useTranslation
```

## Examples

```tsx
// src/routes/<feature>s/index.tsx
const { t } = useTranslation();
const { confirm, alert } = useDialogs();
const navigate = useNavigate();
const [cellSelectionModel, setCellSelectionModel] = useState<GridCellSelectionModel>({});

const { data, isLoading } = useLiveQuery((q) =>
  q.from({ feature: featureCollection }).orderBy(({ feature }) => feature.name, "asc"),
);

const columns: Array<GridColDef<Feature>> = [
  {
    field: "name",
    headerName: t("Feature.Name"),
    width: 200,
    editable: true,
    renderHeader: EditableColumnHeader,
  },
  {
    field: "tags",
    headerName: t("Feature.Tags"),
    width: 200,
    renderCell: (params) => <ChipInputCell params={params} />,
  },
  {
    field: "status",
    headerName: t("Feature.Status"),
    width: 200,
    type: "singleSelect",
    valueOptions: featureStatusEnum.enumValues.map((status) => ({
      value: status,
      label: t(`FeatureStatus.${status}`),
    })),
  },
];

const handleOnDelete = createOptimisticAction<Array<string>>({
  onMutate: (ids) => {
    ids.forEach((id) => {
      featureCollection.delete(id);
    });
  },
  mutationFn: async (ids) => {
    deleteFeatures({ data: ids });
    await featureCollection.utils.refetch();
  },
});

return (
  <Fullscreen>
    <DataGridPremium
      rows={data}
      columns={columns}
      showToolbar
      loading={isLoading}
      ignoreDiacritics
      onRowDoubleClick={({ row }) => navigate({ to: "/features/$id", params: { id: row.id } })}
      cellSelection
      disableRowSelectionOnClick
      cellSelectionModel={cellSelectionModel}
      onCellSelectionModelChange={(newModel) => setCellSelectionModel(newModel)}
      slots={{ toolbar: DataGridToolbar }}
      slotProps={{
        toolbar: {
          title: t("Feature.Features"),
          actions: [
            <Tooltip key="create" title={t("Create")}>
              <RouterIconButton to="/features/create">
                <Add fontSize="small" />
              </RouterIconButton>
            </Tooltip>,
            Object.keys(cellSelectionModel).length === 1 && (
              <Tooltip key="edit" title={t("Edit")}>
                <RouterIconButton
                  to="/features/edit/$id"
                  params={{ id: Object.keys(cellSelectionModel)[0].toString() }}
                >
                  <Edit fontSize="small" />
                </RouterIconButton>
              </Tooltip>
            ),
            Object.keys(cellSelectionModel).length > 0 && (
              <Tooltip key="delete" title={t("Delete")}>
                <IconButton
                  onClick={async () => {
                    const confirmed = await confirm(
                      t("DeleteConfirmation", { count: Object.keys(cellSelectionModel).length }),
                    );
                    if (confirmed) {
                      try {
                        const transaction = handleOnDelete(
                          Object.keys(cellSelectionModel).map((id) => id.toString()),
                        );
                        await transaction.isPersisted.promise;
                        setCellSelectionModel({});
                      } catch (error) {
                        if (error instanceof Error) await alert(error.message);
                      }
                    }
                  }}
                >
                  <Badge
                    badgeContent={Object.keys(cellSelectionModel).length}
                    invisible={Object.keys(cellSelectionModel).length <= 1}
                    color="error"
                  >
                    <Delete fontSize="small" />
                  </Badge>
                </IconButton>
              </Tooltip>
            ),
          ],
        },
      }}
    />
  </Fullscreen>
);
```
