TFW: функция, которую вы хотите, не бесплатна

Скажем, вы действительно хотите использовать разбиение на страницы на стороне сервера с AG React Data Grid… но у вас есть доступ только к версии сообщества…

«Е» означает «только для предприятий» или, возможно, «зло».

Разрушение проблемы

Как было сказано, нам нужна разбивка на страницы на стороне сервера. Для нашего конкретного варианта использования было еще одно требование. Мы хотели, чтобы таблица вела себя как контролируемый компонент, т. е. мы хотели, чтобы данные строки передавались из родительского компонента и могли напрямую изменять эти данные. Между прочим, корпоративная версия на самом деле не сильно помогла бы в этом отношении, поскольку данные строки на стороне сервера в сетке данных реакции предназначены для загрузки через обратный вызов, указанный в свойстве источника данных. Однако мы по-прежнему хотим, чтобы таблица запускала загрузку новой страницы. Поэтому мы хотели, чтобы процесс загрузки новых данных был следующим:

  1. Выполняется переход на еще не загруженную страницу.
  2. Таблица определяет, что мы перешли на эту незагруженную страницу, и запускает асинхронный обратный вызов для загрузки данных новой строки.
  3. Данные новой строки возвращаются в таблицу.
  4. Таблица объединяет новые данные строки в полный массив данных строки.
  5. Таблица вызывает обратный вызов onChange, поэтому новые данные строки могут быть обновлены в состоянии React.
  6. Обновленное состояние данных строки передается обратно в таблицу и отображается.

Контролируемая таблица

Мы создадим новый компонент React с именем ControlledTable, чтобы содержать логику для нашей разбитой на страницы таблицы. Однако сейчас мы забудем об асинхронной загрузке и просто предположим, что все наши данные строк передаются в таблицу одновременно.

import React, { useEffect, useCallback, useState } from "react";
import { AgGridReact } from "ag-grid-react";
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-material.css";
const ControlledTable = ({
  rows,
  totalCount,
  pageSize,
  pageNumber,
  columnDefs,
  getRowNodeId,
  onPageNumberChange,
}) => {
  const [gridApi, setGridApi] = useState();
  let paginationProps = {
    pagination: true,
    paginationPageSize: pageSize,
    cacheBlockSize: pageSize
  };
  const onGridReady = ({ api }) => setGridApi(api);
  const onPaginationChanged = useCallback(
    ({ newPage }) => {
      if (!gridApi || !newPage) {
        return;
      }
      const currentPage = gridApi.paginationGetCurrentPage();
      onPageNumberChange(currentPage);
    },
    [gridApi, onPageNumberChange]
  );
  useEffect(() => {
    if (!gridApi || isNaN(pageNumber)) {
      return;
    }
    const currentPage = gridApi.paginationGetCurrentPage();
    if (pageNumber === currentPage) {
      return;
    }
    gridApi.paginationGoToPage(pageNumber);
  }, [gridApi, pageNumber]);
  return (
    <div className="ag-theme-material" style={{ height: "300px" }}>
      <AgGridReact
        defaultColDef={{
          flex: 1,
          minWidth: 100
        }}
        columnDefs={columnDefs}
        rowData={rows}
        rowCount={totalCount}
        onPaginationChanged={onPaginationChanged}
        getRowNodeId={getRowNodeId}
        onGridReady={onGridReady}
        {...paginationProps}
      />
    </div>
  );
};
export default ControlledTable;

Для этой первой версии мы передали следующие реквизиты:

rows: полные данные строки.

totalCount: количество строк.

pageSize: количество строк на странице.

pageNumber: номер текущей страницы.

columnDefs: определение полей в данных строки.

getRowNodeId: функция для создания уникального идентификатора из строки.

onPageNumberChange: обратный вызов, вызываемый при изменении пользователем номера страницы.

У нас уже есть контролируемый компонент, потому что данные строки, количество строк и номер страницы передаются через свойства, и существует обратный вызов для обновления номера страницы. Нам просто нужен способ запустить процесс асинхронной загрузки для новых данных строки.

Добавление асинхронной загрузки данных

Мы хотим определить асинхронную функцию для захвата указанного блока строк, которую мы можем передать нашему ControlledTable в качестве свойства. Мы должны определиться с сигнатурой метода. Довольно часто запрашиваются результаты с разбивкой на страницы с использованием параметров limit и offset, чтобы указать, сколько строк нужно вернуть и с какого смещения начать. Мы хотим, чтобы функция просто разрешала блок запрошенных строк.

Если мы хотим, чтобы ControlledTable обновлял полные данные строки после получения запрошенного блока, нам нужно добавить обратный вызов onChange для его вызова. Обратный вызов onChange, в свою очередь, скажет родительскому компоненту обновить данные строки в состоянии.

const ControlledTable = ({
  rows,
  totalCount,
  pageSize,
  pageNumber,
  columnDefs,
  getRowNodeId,
  onPageNumberChange,
+ getRows,
+ onChange,
}) => {

Чтобы компонент AgGridReact правильно отображал элементы управления страницы с неполными данными, нам нужно заполнить строки строками-заполнителями. Нам нужно, чтобы фактическая длина массива соответствовала totalCount. Мы создадим функцию для создания строк-заполнителей

const getPlaceholderItems = (startRow, length) => {
  const items = [];
  for (let index = startRow; index < length; index += 1) {
    items.push({ index, placeholder: true });
  }
  return items;
};

и мы будем заполнять строки перед передачей их компоненту AgGridReact.

if (rows?.length < totalCount) {
  rows.splice(
    rows.length,
    0,
    ...getPlaceholderItems(rows.length, totalCount)
  );
}

Нам также необходимо рассмотреть, как на самом деле управлять жизненным циклом открытия страницы —> вызов getRows —> объединение строк в массив —> вызов обратного вызова onChange.

Мы хотим отслеживать, какие блоки нам нужно загрузить, а какие уже загружаются. Мы определим переменную состояния loadingBlocks, которая будет содержать массив номеров строк. Номера строк указывают первую строку в блоке, который мы хотим загрузить.

Мы определим функцию для проверки необходимости загрузки блока, начинающегося со строки startRow:

const needsLoading = useCallback(
  (startRow) => {
    if (!rows?.length) {
      // We need to load if rows are completely empty
      return true;
    }
    const max = Math.min(startRow + pageSize, rows.length);
    for (let i = startRow; i < max; i += 1) {
      if (isPlaceholder(i)) {
        return true;
      }
    }
  },
  [rows, pageSize, isPlaceholder]
);

isPlaceholder — это функция, которая просто проверяет, является ли строка одной из строк-заполнителей, которыми мы дополняли строки:

const isPlaceholder = useCallback((i) => !rows[i] || rows[i].placeholder, [
  rows
]);

Мы будем использовать needsLoading в useEffect, который запускается при изменении номера страницы:

useEffect(() => {
  const startRow = pageNumber * pageSize;
  if (!loadingBlocks.includes(startRow) && needsLoading(startRow)) {
    // We haven't started loading the block yet. Start loading it
    setLoadingBlocks([...loadingBlocks, startRow]);
  }
}, [loadingBlocks, pageNumber, pageSize, needsLoading]);

Мы видим, что если текущий блок нуждается в загрузке, а мы еще не добавили его в loadingBlocks, мы добавим его в loadingBlocks. Итак, теперь в состояние добавлены блоки, которые нуждаются в загрузке.

Итак, теперь у нас есть способ отслеживать, какие блоки нуждаются в загрузке, и нам нужно использовать эту информацию, чтобы запустить процесс загрузки. Начнем с создания скелета useEffect

for (const startRow of loadingBlocks) {
  useEffect(() => {
    console.log("startRow", startRow);
  });
}

и давайте взглянем на консоль...

Warning: React has detected a change in the order of Hooks called by ControlledTable. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Previous render            Next render
   ------------------------------------------------------
1. useState                useState
2. useCallback             useCallback
3. useEffect               useEffect
4. useCallback             useCallback
5. useCallback             useCallback
6. useEffect               useEffect
7. undefined               useEffect

Уф, не понравилось. React выдаст ошибку и откажется от рендеринга, если мы изменим количество или порядок хуков. К счастью, есть обходной путь. Мы создадим невидимый компонент, то есть тот, который на самом деле ничего не отображает, чтобы обернуть useEffect и загрузить наши данные.

const LoadingBlock = ({ getRows, pageSize, startRow, onLoaded }) => {
  const [rows, setRows] = useState();
  useEffect(() => {
    let cleaningUp;
    getRows(pageSize, startRow).then((rows) => {
      if (cleaningUp) {
        return;
      }
      setRows(rows);
    });
    return () => {
      cleaningUp = true;
    };
  }, [getRows, pageSize, startRow]);
  useEffect(() => {
    if (!rows) {
      return;
    }
    onLoaded(rows, startRow);
  }, [onLoaded, rows, startRow]);
  return null;
};

Это довольно простой компонент. У него есть один useEffect, ответственный за вызов обратного вызова getRows и обновление локальной переменной состояния rows. Другой useEffect отвечает на обновление rows и вызывает обратный вызов onLoaded. Мы хотим разделить их на несколько useEffect, потому что обратный вызов onLoaded может обновляться в процессе загрузки, и мы не хотим запускать первый useEffect, когда это произойдет.

Единственная его часть, которая может показаться немного странной, — это переменная cleaningUp. Если переменная состояния реакции обновляется после размонтирования компонента, в консоль будет записана ошибка. Поэтому мы будем использовать cleanUp, чтобы отслеживать, когда следует избегать обновления состояния. Мы включим его в функции очистки useEffect.

Нам понадобится обратный вызов onLoaded, чтобы передать LoadingBlock.

const onLoaded = useCallback(
  (newRows, startRow) => {
    // We've loaded the block. Update the rows array
    let rowsCopy = [...rows];
    rowsCopy.splice(startRow, pageSize, ...newRows);
    const newLoadingBlocks = [...loadingBlocks];
    newLoadingBlocks.splice(newLoadingBlocks.indexOf(startRow), 1);
    setLoadingBlocks(newLoadingBlocks);
    onChange(rowsCopy);
  },
  [rows, pageSize, loadingBlocks, onChange]
);

Итак, этот обратный вызов делает две вещи. Он удаляет блок загрузки из массива блоков загрузки, чтобы удалить соответствующий компонент LoadingBlock. Он также объединяет загруженные данные строки в полный массив строк перед передачей этих обновленных данных строки обратному вызову onChange.

Последнее, что нам нужно сделать, это просто отрендерить эти LoadingBlock компонента.

{loadingBlocks.map((startRow) => (
  <LoadingBlock
    key={`loadingBlock:${startRow}`}
    getRows={getRows}
    startRow={startRow}
    pageSize={pageSize}
    onLoaded={onLoaded}
  />
))}

Загрузка оверлея

Еще одна необязательная функция, которую мы должны добавить, — это предотвращение взаимодействия пользователя со строками во время их загрузки. Добавим еще один useEffect.

useEffect(() => {
  if (!gridApi) {
    return;
  }
  if (loadingBlocks.includes(pageNumber * pageSize)) {
    gridApi.showLoadingOverlay();
  } else {
    gridApi.hideOverlay();
  }
}, [gridApi, pageNumber, pageSize, loadingBlocks]);

Когда текущая страница загружается, мы показываем оверлей загрузки. В противном случае мы его спрячем.

Веселиться!

Были сделаны. Поиграйте с готовой версией в CodeSandbox.