Этот пост является продолжением моей статьи Использование React Query с React Table. Я рекомендую сначала прочитать это, чтобы понять полный контекст.

Мы также будем следовать тому же набору примеров кода, который можно найти на github.com/nafeu/react-query-table-sandbox. Мы сосредоточимся на файле ReactTableExpanding.jsx.

Вы можете быстро настроить его с помощью:

git clone https://github.com/nafeu/react-query-table-sandbox.git
cd react-query-table-sandbox
npm install
npm start

Расширяемый стол, который мы хотим построить

Понимание наших данных

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

[
  { id: 1, name: 'foo' },
  { id: 2, name: 'bar' },
  { id: 3, name: 'baz' }
]

И в таблице отобразит:

Но что, если бы у нас была более иерархическая (или древовидная) структура, такая как:

[
  {
    id: 1,
    name: 'foo'
    children: [
      { id: 4, name: 'qux' },
      { id: 5, name: 'quux' }
    ]
  },
  { id: 2, name: 'bar' },
  { id: 3, name: 'baz' }
]

Здесь все становится шатким, и нам нужно немного изменить наш подход. Допустим, мы хотим отобразить это, но отображаем children только тогда, когда мы нажимаем на отдельную строку. Например, если бы мы щелкнули по строке с id: 1, мы бы хотели увидеть плоскую таблицу, отображаемую следующим образом:

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

В index.mjs нашего примера кода мы определили API, который возвращает фиктивные данные о воображаемой группе обсуждения разработчиков программного обеспечения.

Конечная точка api/ возвращает данные в форме:

[
  {
    "id": 1,
    "name": "How to use react-table in reporting dashboard",
    "active": 40,
    "status": "locked",
    "upvotes": 30
  },
  {
    "id": 2,
    "name": "How to use react-query for BI solution",
    "active": 31,
    "status": "resolved",
    "upvotes": 39
  }
  ...
]

Конечная точка api/child возвращает аналогичные данные, но каждый раз предоставляет новые записи, тогда как конечная точка корневого api/ возвращает исходную полезную нагрузку с небольшими изменениями. Это сделано для имитации «активного веб-сайта», на котором данные изменяются в псевдореальном времени.

Хотя пример немного грубоват, представьте на мгновение, что каждая из этих тем обсуждения имеет подвопросы, которые структурированы одинаково. Они будут использоваться для сохранения «иерархической» структуры в нашей таблице.

В нашем внешнем коде мы имеем:

const fetchParentData = () => axios.get(`http://localhost:8000/api`);
const fetchChildData = () => axios.get(`http://localhost:8000/api/child`);

Это помощники API на основе обещаний, которые извлекают частично динамические родительские данные и новый набор дочерних данных соответственно (для React Query необходимо использовать основанные на обещаниях помощники API).

Рекурсивное изменение данных строки

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

row-0
row-1
row-2

to

row-0
  row-0.0
  row-0.1
row-1
row-2

и даже дальше

row-0
  row-0.0
    row-0.0.0
    row-0.0.1
  row-0.1
row-2
row-3

и так далее и тому подобное. В нашем примере кода мы делаем это с помощью функции recursivelyUpdateTable, которая выглядит следующим образом:

Здесь мы берем:

  • tableData - ›existingRows это наши существующие данные таблицы
  • childData - ›subRowsToInsert, который представляет новые строки, которые мы хотим вставить
  • id - ›path, который отформатирован как x.y.z и разделен на [x, y, z], где x, y и z могут быть индексами, представляющими определенные строки в коллекции, а весь id фактически служит path для определенного node (строки),

Мы используем эти значения и некоторые базовые знания рекурсивных алгоритмов, чтобы сначала получить наш текущий путь для обхода:

const id = path[0];

Затем мы формализуем наш базовый случай (который в нашем случае - это когда остался только один индекс пути), решаем, есть ли у достигнутого узла подстроки или нет, а затем вставляем строку:

Если мы не в нашем базовом случае, мы рекурсивно используем функцию insertIntoTable и загружаем версию подмножества наших текущих строк:

Это может быть немного сложно, особенно если вы не писали много рекурсивного кода в течение некоторого времени, поэтому не стесняйтесь не торопиться, чтобы проследить и понять его.

Цель этой статьи не в том, чтобы сосредоточиться на рекурсии, а в том, чтобы сосредоточиться на использовании React Table для создания расширяемых строк с отложенной загрузкой. Однако этот помощник по-прежнему остается очень важной его частью.

Основываясь на нашем запросе React + предложение React Table

В моем предыдущем посте я предложил схему объединения React Query и React Table, этот код также существует в нашем примере кода на ReactQueryWithTable.jsx

Предложение состоит из следующего структурирования уровня запроса, уровня обработки данных и уровня рендеринга между тремя компонентами:

TableQuery
TableInstance
TableLayout

Давайте построим или расширим этот пример, обратитесь к ReactTableExpanding

Обновление нашего компонента TableQuery

Давайте посмотрим на наш TableQuery компонент. Мы создаем новую переменную с именем isRowLoading и реализуем функцию-обработчик handleClickRow, которая будет вызываться каждый раз при нажатии отдельной строки в нашей таблице.

Следует отметить, что isRowLoading - это переменная состояния загрузки. Это сопоставление paths с логическим значением, которое помогает нам отслеживать, какая строка в данный момент находится в процессе выборки данных:

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

У нас также есть вызов useQuery, который мы изменяем:

const {
  data: apiResponse,
  isLoading
} = useQuery('discussionGroups', fetchParentData, { enabled: !tableData });

Мы добавляем enabled: !tableData, чтобы предотвратить повторную выборку табличной формы в фоновом режиме, так как это может испортить встроенную структуру. Реализация повторной выборки со встроенной структурой - это более сложная тема, которая не будет рассматриваться в этой статье.

Мы также передаем еще несколько реквизитов в TableInstance, например:

return (
  <TableInstance
    tableData={tableData}
    onClickRow={handleClickRow}
    isRowLoading={isRowLoading}
  />
);

Обновление нашего компонента TableInstance

Помимо использования двух новых свойств onClickRow и isRowLoading, мы в основном сосредоточимся на новом заголовке столбца, который мы добавляем:

Вот где происходит некоторая магия. Мы используем свойство Cell, чтобы указать React Table, как следует форматировать отдельную ячейку, в этом случае мы добавляем дополнительный столбец перед нашей таблицей, который будет содержать кнопку расширения, а также добавляем некоторую дополнительную логику для запуска нашего пользовательского onClickRow обработчик, если строка находится в соответствующем состоянии загрузки и раскрытия.

Вам может быть интересно, откуда взялся { row, isLoading, isExpanding }? Объект row на самом деле является тем, что React Table предоставляет нам для доступа при использовании свойства Cell, это очень полезно для нас, поскольку row также имеет полезный метод под названием getToggleRowExpandedProps(), который мы можем использовать для ручного запуска функции расширения строки React Table.

Мы также добавляем ловушку useExpanded в экземпляр нашего экземпляра таблицы с параметром autoResetExpanded, установленным в false:

const tableInstance = useTable(
  { columns, data, autoResetExpanded: false },
  useExpanded
);

И мы передаем объект isRowLoading в TableLayout:

return (
  <TableLayout
    {...tableInstance}
    isRowLoading={isRowLoading}
  />
);

Обновление нашего компонента TableLayout

В нашем TableLayout компоненте мы извлекаем дополнительную опору state.expanded из нашего tableInstance. Это предоставляется нам React Table благодаря хук useExpanded, он обеспечивает сопоставление, подобное тому, как мы определили наше сопоставление isRowLoading.

const TableLayout = ({
  getTableProps,
  getTableBodyProps,
  headerGroups,
  rows,
  prepareRow,
  isRowLoading,
  state: { expanded }
}) => {
  ...
}

Основное обновление заключается в отрисовке отдельных строк:

return (
  <tr {...row.getRowProps()}>
    {row.cells.map(cell => {
      return <td {...cell.getCellProps()}>
        {cell.render('Cell', {
          isLoading: isRowLoading[row.id],
          isExpanded: expanded[row.id]
        })}
      </td>
    })}
  </tr>
)

В методе cell.render мы добавляем дополнительный объект, который заполняется:

{
  isLoading: isRowLoading[row.id],
  isExpanded: expanded[row.id]
}

Это работает в тандеме с нашим ранее объявленным кодом в компоненте TableInstance, где мы можем передавать пользовательские значения непосредственно в Cell. Мы сами определили isRowLoading - ›isLoading и подаем expanded -› isExpanded, чтобы обеспечить выполнение состояния загрузки и развернутого состояния. Это даже не так сложно читать, это то, что делает React Table такой невероятной для работы.

Когда вы соберете все это вместе, у вас получится вот такой шикарный стол:

Удачного кодирования!