Best Practices

Do's

  1. Define columns outside the component. Column definitions should be stable references. Declaring them at module scope (or inside a useMemo) prevents TanStack Table from recalculating derived state on every render.
  2. Memoize data transformations. If you transform or filter your data before passing it to DataTable, wrap that logic in useMemo so the table only re-renders when the underlying source changes.
  3. Use createTableConfig() for shared configs. The factory function validates your config at creation time and freezes the object, making it safe to share across multiple table instances without risk of mutation.
  4. Use the selector pattern with useTableConfig. When reading config values inside components, use selectors to subscribe to only the slice of config you need, minimising unnecessary re-renders.
  5. Leverage feature flags to reduce DOM. Disabling features you do not need (e.g. cardView, advancedFilter, rowSelection) prevents those components from mounting entirely, reducing both DOM size and event listener count.
  6. Use server-side mode for large datasets (5,000+ rows). Client-side filtering and sorting of thousands of rows creates measurable jank. Offload this work to your API and pass only the current page to the table.
  7. Provide aria-label on custom columns. Columns with icon-only or non-text headers should include an aria-label in their column definition so screen readers can identify them.
  8. Set enableSorting: false and enableHiding: false on action and select columns. These columns serve a structural purpose; allowing them to be sorted or hidden leads to a confusing user experience.
  9. Use discriminated union props correctly. TableCraft uses discriminated unions to separate server-side and client-side prop shapes. Pass the correct variant to get full TypeScript inference and avoid runtime mismatches.
  10. Use the config prop for per-instance overrides. When most tables share a provider config but one table needs different behaviour, pass a config object directly to that table instead of restructuring your provider setup.

Don'ts

  1. Do not create new config objects on every render. Inline object literals passed as config props break referential equality and trigger unnecessary re-renders throughout the table. Define configs at module scope or inside a useMemo.
  2. Do not use client-side filtering with server-side pagination. Mixing the two modes means the table filters only the current page rather than the full dataset, producing incorrect results. Choose one mode and use it consistently.
  3. Do not mutate DEFAULT_TABLE_CONFIG. The default config is frozen with Object.freeze. Attempts to mutate it will silently fail in non-strict mode or throw in strict mode. Always derive new configs using createTableConfig().
  4. Do not mix isQueryFilter with filterableColumns. These are mutually exclusive filtering strategies. Using both simultaneously leads to conflicting state and unpredictable filter behaviour.
  5. Do not forget to reset pagination on filter changes in server-side mode. When the user applies a filter, the total number of results changes. If you do not reset the page index to 0, users may see an empty page because the current offset exceeds the new result count.
  6. Do not add select or action columns to searchable or filterable column lists. These columns contain UI controls, not data values. Including them in search or filter configuration will produce meaningless results and may cause runtime errors.
  7. Do not rely on URL params as the primary source of truth for server-side state. URL sync is a convenience feature for shareability, not the canonical state store. Drive your server queries from the table's internal state callbacks and treat the URL as a secondary reflection.
  8. Do not nest multiple TableProviders unnecessarily. Each provider creates its own context scope. Nesting providers when a single top-level provider suffices adds complexity and may cause inner tables to unexpectedly inherit or shadow outer config.