svelte-headless-table icon indicating copy to clipboard operation
svelte-headless-table copied to clipboard

Race condition between the `groupBy` plugin and svelte rerender

Open lolcabanon opened this issue 11 months ago • 1 comments

There seems to be somewhat of a race condition (not sure it's the right term, but it illustrates the problem) between the groupBy plugin and svelte rerender (because of the {#each} block maybe?).

Here's the actual problem :

When I trigger a group operation (hCellProps.group.toggle()) on some columns (not all, seemingly[^1]), some $originalRows are displayed in place of the $rows (or $pageRows) that should show.

[^1]: When there are around 10 groups or more, it tends to happen frequently.

Notes and observations :

  • The same originalRows are showing each time (in my case, rows 5, 6, 8, 9, 10)
  • The grouping occurs correctly if the same column key is set in initialGroupByIds
  • Then if I ungroup / regroup, chaos starts in the table
  • But the $pageRows store is dumped with correct columns (see <pre> tags in the bottom)

A peek of my current code : (I'll try to put a reproduction somewhere when I have more time)

<script>
// ...

const table = createTable(readable(data), {
  select: TablePlugins.addSelectedRows(),

  columnFilter: TablePlugins.addColumnFilters(),
  tableFilter: TablePlugins.addTableFilter({
    fn: ({ filterValue, value }) => {
      return value.toLowerCase().includes(filterValue.toLowerCase());
    }
  }),

  sort: TablePlugins.addSortBy({
    isMultiSortEvent: isShiftClick,
    initialSortKeys: [{ id: 'createdAt', order: 'desc' }]
  }),

  group: TablePlugins.addGroupBy({
    // disableMultiGroup: true
    isMultiGroupEvent: isShiftClick
    // initialGroupByIds: ['statutActuelStr']
  }),

  expanded: TablePlugins.addExpandedRows({}),

  resize: TablePlugins.addResizedColumns(),
  grid: TablePlugins.addGridLayout(),
  pagination: TablePlugins.addPagination({
    initialPageSize: 100
  }),
});

let columns = table.createColumns({
  table.display({  // this column shows expand button if rows are grouped
    id: 'expand',
    header: (headerCell, { pluginStates }) =>
      createRender(RowsUngrouper, {
        groupByIds: pluginStates.group.groupByIds
      }),
    cell: ({ row }, { pluginStates }) => {
      const { canExpand, isExpanded } =
        pluginStates.expanded.getRowState(row);
      return createRender(RowExpander, { isExpanded, canExpand });
    },
    plugins: {
      resize: {
        disable: true,
        initialWidth: CELL_WIDTH.MIN
      },
    }
  }),

  // ...

  table.column({  // this column is the one I group by
    header: 'Statut',
    accessor: 'statutActuelStr',
    cell: statutCell,
    plugins: {
      group: {
        getGroupOn: (v) => (v === null ? 'Indéterminé' : v)
      },
      columnFilter: txtColFilter,
      resize: {
        initialWidth: CELL_WIDTH.LARGE,
        minWidth: CELL_WIDTH.MED
      }
    }
  }),

  // ...
});

const tableViewModel = table.createViewModel(columns, {
  rowDataId: (item, _index) => item.id.toString()
});
</script>

<table>
  <thead>
    ...
  </thead>
  <tbody>
  {#each $pageRow as pageRow, i (pageRow.id)}
    <Subscribe
      pageRowAttrs={pageRow.attrs()}
      let:pageRowAttrs
      pageRowProps={pageRow.props()}
      let:pageRowProps
    >
      <tr {...pageRowAttrs} data-row-index={i} on:click={() => rowClicked(pageRow)}>
        {#each pageRow.cells as cell (cell.id)}
          <TBodyCell {cell} />
          {/each}
        </tr>
      </Subscribe>
  {/each}
  </tbody>
</table>

<!-- for debugging : here rows seems to be correctly shown after a rerender -->
<section>
  <details>
    <summary>$originalRows</summary>
    <pre>{JSON.stringify(
      $originalRows.map(
        (r, i) =>
          `(${String(i).padStart(3, ' ')}) id : ${r.id.padStart(6, ' ')}, nom : ${r.cells.find((c) => c.id == 'prenom')?.value ?? 'Inconnu'}`
          ),
          getCircularReplacer(),
          2
      )}</pre>
  </details>
  <details>
    <summary>$rows</summary>
    <pre>{JSON.stringify(
      $rows.map(
        (r, i) =>
          `(${String(i).padStart(3, ' ')}) id : ${r.id.padStart(6, ' ')}, nom : ${r.cells.find((c) => c.id == 'prenom')?.value ?? 'Inconnu'}`
          ),
          getCircularReplacer(),
          2
      )}</pre>
  </details>
  <details>
    <summary>$pageRows</summary>
    <pre>{JSON.stringify(
      $pageRows.map(
        (r, i) =>
          `(${String(i).padStart(3, ' ')}) id : ${r.id.padStart(6, ' ')}, nom : ${r.cells.find((c) => c.id == 'prenom')?.value ?? 'Inconnu'}, cells : 
          ${r.cells.map((c) => c.value).join()}`
        ),
        getCircularReplacer(),
        2
      )}</pre>
  </details>
</section>

Dirty fix :

<script>
const getRows = async (current) => await new Promise((res) => setTimeout(() => res(current), 0));
</script>

<tbody {...$tableBodyAttrs}>
  {#await getRows($pageRows)}
    Loading...
  {:then finalRows}
    {#each finalRows as row, i (row.id)}
      ...
    {/each}
  {/await}
<tbody/>

Versions :

{
  "@sveltejs/kit": "^2.5.4",
  "svelte": "^4.2.12",
  "svelte-headless-table": "^0.18.2"
}

Screenshots :

image

image

image

lolcabanon avatar Mar 22 '24 23:03 lolcabanon