Skip to content

DataTable with transform infinite-renders and freezes the tab (sort/filter) #569

Description

@felipe-demolin

Summary

In @databricks/appkit-ui, DataTable / TableWrapper calls transform(data) on every render and passes the result straight to useReactTable as data. A typical rows.map(...) returns a new array identity each time. TanStack Table treats that as a data change, auto-resets pageIndex, React re-renders, and the tab freezes.

Sorting is just the first user action that re-renders. Filter, pagination, and any parent state change do the same.

Confirmed in 0.38.1 (reproduced in a production app) and still present in 0.66.1 (packages dist table-wrapper.js).

There is no React "maximum update depth" overlay: TanStack schedules the page-index reset through microtasks, so the main thread just locks. CDP samples during the freeze sit in performSyncWorkOnRootTableWrapper. Network is idle.

Reproduction

<DataTable
  queryKey="some_query"
  parameters={{}}
  transform={(rows) => rows.map((row) => ({ ...row, n: Number(row.n) }))}
/>
  1. Wait for rows to load.
  2. Click any column header to sort.
  3. The tab becomes unresponsive. No error, no extra analytics requests.

Without transform, sorting works.

Expected

transform should run when query data changes, not on every render. Sort, filter, and pagination should stay interactive.

Root cause

TableWrapper currently does:

const processedData = hasData ? (transformer ? transformer(data) : data) : [];
useReactTable({ data: processedData, /* ... */ });

columns is also useMemo'd with processedData in the dependency list, so a new array also rebuilds column defs every render.

useAnalyticsQuery already solves a similar identity problem with useStableParams. transform has no equivalent.

TanStack's default autoResetPageIndex turns "new array every render" into a setState loop instead of just wasted work.

Suggested fix

Memoize processed rows on query data, and keep the latest transformer in a ref so an inline transform prop does not retrigger the memo:

const transformerRef = useRef(transformer);
transformerRef.current = transformer;

const processedData = useMemo(() => {
  if (!data?.length) return [];
  return transformerRef.current ? transformerRef.current(data) : data;
}, [data]);

Also:

  • Memoize generated columns on column keys / first-row shape, not on the new processedData array.
  • Optionally set autoResetPageIndex: false, or only reset when data actually changes.

useCallback on the consumer's transform is not sufficient with the current code: even a stable function still returns a new array, and that array is not memoized.

Workaround

Cache transform output by input-array identity (useRef), or skip transform and reshape in SQL.

Environment

  • @databricks/appkit / @databricks/appkit-ui 0.38.1 (repro), 0.66.1 (source still has the pattern)
  • React 19
  • DataTable opinionated mode with a transform prop

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions