Подключение таблицы
В серверном режиме таблица перестаёт что-либо вычислять сама: она отдаёт наружу своё состояние и принимает готовую страницу строк. Эта страница — про то, как соединить её с эндпоинтом так, чтобы контракт сошёлся с обеих сторон.
Включить серверный режим
По умолчанию DataGrid пагинирует, сортирует и фильтрует данные, которые у неё уже есть в памяти. Три пропса переводят её в режим «за это отвечает сервер»:
<DataGrid
paginationMode="server"
sortingMode="server"
filterMode="server"
rowCount={rowCount}
{...rest}
/>rowCount обязателен: без общего числа строк пагинатор не знает, сколько страниц
показывать, и решит, что данные закончились на первой. Именно это значение возвращает
MuiGridResponse::$rowCount.
Три режима включаются независимо
Забыть один из трёх — типичная ошибка. Если оставить filterMode клиентским, таблица
отфильтрует уже пришедшую страницу: пользователь наберёт условие и увидит пустоту,
хотя на сервере совпадения есть.
Отправить состояние
Состояние таблицы приходит тремя коллбэками. Собираем их в одно тело запроса:
const [rows, setRows] = useState([]);
const [rowCount, setRowCount] = useState(0);
const [loading, setLoading] = useState(false);
const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 25 });
const [sortModel, setSortModel] = useState([]);
const [filterModel, setFilterModel] = useState({ items: [] });
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch('/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({
page: paginationModel.page,
pageSize: paginationModel.pageSize,
sortModel,
filterModel,
}),
})
.then((r) => r.json())
.then((data) => { setRowCount(data.rowCount); setRows(data.rows); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [paginationModel, sortModel, filterModel]);AbortController здесь не украшение: пользователь листает страницы быстрее, чем отвечает
сервер, и без отмены ответы приходят вперемешку — таблица показывает предыдущую страницу
поверх текущей.
Контракт имён
Значение field у колонки таблицы и имя поля в схеме — одна и та же строка.
const columns = [
{ field: 'title', headerName: 'Заголовок', flex: 1 },
{ field: 'views', headerName: 'Просмотры', type: 'number' },
{ field: 'authorName', headerName: 'Автор' },
];GridSchema::make(
GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable(),
GridColumn::for('views', 'a.views')->filterable(FilterType::Number)->sortable(),
GridColumn::for('authorName', 'au.name')->filterable(FilterType::String)->sortable(),
);Расхождение проявляется по-разному в зависимости от операции: фильтр по незнакомому полю
вернёт 400, а сортировка по нему молча не сработает — таблица будет выглядеть исправной,
но клик по заголовку ничего не изменит. Причины этой асимметрии — в
Ментальной модели.
Совпадать должны имена, а не типы
type: 'number' в колонке таблицы влияет только на то, какие операторы предложит
интерфейс. Сервер полагается на FilterType в схеме, поэтому шлюз по типу работает даже
для запросов в обход интерфейса.
Идентификатор строки
DataGrid требует, чтобы у каждой строки было уникальное поле id. Если ваш SELECT его не
возвращает или называет иначе — укажите таблице, где его искать:
<DataGrid rows={rows} getRowId={(row) => row.uuid} … />Проще всего этого избежать: включить первичный ключ в select(), даже если колонки для
него в таблице нет. Он поедет в браузер, но показан не будет.
->select('a.id, a.title, a.views, au.name author_name')Придать строкам форму
Четвёртый аргумент wrap() — маппер, который прогоняется по строкам текущей страницы
(не по всей таблице). Это правильное место, чтобы превратить плоскую строку из базы в тот
объект, который удобен фронтенду:
final readonly class ArticleRow
{
public function __construct(
public int $id,
public string $title,
public ?array $author,
) {
}
public static function from(object $r): self
{
return new self(
id: $r->id,
title: $r->title,
author: $r->author_id ? ['id' => $r->author_id, 'name' => $r->author_name] : null,
);
}
}
return MuiGrid::wrap($repo, $request, $schema, fn ($r) => ArticleRow::from($r));Результат
{
"rowCount": 134,
"rows": [
{ "id": 12, "title": "Winter internals", "author": { "id": 3, "name": "Ada" } }
]
}Вложенное поле в таблице показывается через valueGetter:
{ field: 'authorName', headerName: 'Автор', valueGetter: (v, row) => row.author?.name }Маппер не меняет фильтрацию
Имена полей в схеме относятся к SQL, а не к результату маппера. Если маппер переименовал
author_name в author.name, фильтровать всё равно нужно по authorName — по тому имени,
которое объявлено в схеме.
Обработка отказов
Отклонённый фильтр приходит как 400 с текстом. Показать его пользователю полезнее, чем
молча оставить пустую таблицу:
.then(async (r) => {
if (!r.ok) throw new Error((await r.json()).message ?? 'Не удалось загрузить данные');
return r.json();
})
.then((data) => { setRowCount(data.rowCount); setRows(data.rows); })
.catch((e) => setError(e.message))На практике 400 от грида означает одно из двух: интерфейс прислал поле, которого нет в
схеме, или оператор, не разрешённый для типа колонки. И то и другое — расхождение
конфигураций, а не действие пользователя, поэтому такие ошибки полезно логировать.
Что дальше
- Колонки из JOIN — колонки из связанных таблиц.
- Фильтры вне грида — строка глобального поиска и другие фильтры, которых нет среди колонок.
- Операторы — что именно присылает интерфейс.