Skip to content

Ambient Context

The context prop is the table’s ambient context: a single bag of values that’s available throughout the table’s customizable parts — EmptyPlaceholder, the loading components, ScrollElement, computeRowKey, structural component overrides. You set it once on <DataTable> and the table passes it to every callback and component override that reads it, without per-callback prop wiring.

Typical contents are values the surrounding screen owns but the table’s slots need to read:

  • the current search query, so EmptyPlaceholder can render “no results for archived
  • the active tenant ID, so computeRowKey can scope keys across saved views
  • a sticky-header offset the parent layout controls, so a custom ScrollElement can honor it
function EmptyPlaceholder({ context }) {
  return <div>{context.emptyMessage}</div>
}

;<DataTable
  EmptyPlaceholder={EmptyPlaceholder}
  computeRowKey={({ data, context }) => `${context.tenantId}:${data.id}`}
  context={{ emptyMessage: 'No archived products', tenantId: 'acme' }}
  model={model}
>
  {/* columns */}
</DataTable>

Compared to wiring each callback to its own props or closing over surrounding state, context is the right channel when several internals consume the same value (a search query the empty state needs to display and the loading state references) or when the value comes from above the table component (a router param, an auth scope, a feature flag).

The same context value reaches:

  • computeRowKey — for scoping row keys across tenants, saved views, or table instances that might share row IDs.
  • EmptyPlaceholder — for the failed-search query, the filter that returned zero rows, or recovery copy.
  • Loading slots (LoadingPlaceholder, LoadingOverlay, LoadingFooter) — for status copy or retry handlers that depend on screen state.
  • Row component overrides — for density flags, alternating-row styling toggles, hover-state preferences.
  • Sticky wrapper overrides — for sticky background colors that match the surrounding chrome.
  • Custom scroll elements — for layout flags, scrollbar styling, or whether to honor a sticky header height.

Cell and header renderers don’t receive context. They run inside the table’s column-virtualization loop, where threading context through every cell on every scroll would be a measurable cost in a wide table. For values cells need to read, close over screen-local state directly when the renderer is inline, or use React context (useContext) when the renderer is extracted:

const FilterContext = React.createContext<string>('')

function ProductCell({ row }: CellRenderParams) {
  const filter = React.useContext(FilterContext)
  return <span>{highlight(row.data.name, filter)}</span>
}

The two channels — React’s useContext and the table’s context prop — coexist cleanly: React context for what cell renderers need, the table’s context prop for what infrastructure needs.

For values defined right above the table in the same component, a closure is simpler:

function ProductsTable({ searchQuery, tenantId }: Props) {
  return (
    <DataTable
      EmptyPlaceholder={() => <div>No products match "{searchQuery}"</div>}
      computeRowKey={({ data }) => `${tenantId}:${data.id}`}
      model={model}
    >
      {/* columns */}
    </DataTable>
  )
}

context becomes the better choice when the callbacks are extracted (a shared EmptyPlaceholder component lives in a design system, not in this file), when one value is consumed by several callbacks at once, or when the table is rendered from a higher-up shell and the callbacks live further down the tree.