Core Guides

Table Context Guide

When you register reusable components with createTableHook, those components read the current table, cell, or header from React context instead of from props. This guide explains how that context works, how to read it with full type-safety, and how to create an isolated context for advanced cases like nested tables.

How The Context Is Provided

The components returned by useAppTable are what put values on context:

  • <table.AppTable> provides the current table instance.

  • <table.AppCell cell={cell}> provides the current cell instance.

  • <table.AppHeader header={header}> and <table.AppFooter header={footer}> provide the current header instance.

    Your registered tableComponents, cellComponents, and headerComponents then read those instances with the matching hooks:

tsx
function PaginationControls() {
  const table = useTableContext() // the table provided by <table.AppTable>
  return <button onClick={() => table.nextPage()}>Next</button>
}

function TextCell() {
  const cell = useCellContext<string>() // the cell provided by <table.AppCell>
  return <span>{cell.getValue()}</span>
}

function SortIndicator() {
  const header = useHeaderContext() // the header provided by <table.AppHeader>
  return header.column.getIsSorted() ? '🔼' : null
}

The instance you read back is the same one those wrappers render with, already extended with the App* components and your registered components. For example, inside a table component table.PaginationControls and table.AppCell are available, and inside a cell component cell.TextCell and cell.FlexRender are available.

Tip

You often do not need table context to reach the table at all. Every cell, header, row, and column instance already carries a reference back to its table: cell.table, header.table, row.table, and column.table. So inside a cell or header component you can read the table straight off the instance you already have (for example cell.table.nextPage()) instead of calling useTableContext(). The instances are linked to each other too (cell.row, cell.column, header.column, row.getAllCells()), so a single instance is usually enough to navigate to whatever you need. Reach for useTableContext() when a component has no instance to start from, such as a standalone toolbar or pagination control.

Import The Hooks From Your createTableHook Result

Important

For the best type-safety, import useTableContext, useCellContext, and useHeaderContext from the same module where you called createTableHook. Those hooks know your TFeatures and your registered component maps, so table.PaginationControls, cell.TextCell, and header.SortIndicator are all typed.

tsx
// hooks/table.ts
export const {
  useAppTable,
  createAppColumnHelper,
  useTableContext, // <- typed with TFeatures + your tableComponents
  useCellContext, //  <- typed with TFeatures + your cellComponents
  useHeaderContext, // <- typed with TFeatures + your headerComponents
} = createTableHook({ features, tableComponents, cellComponents, headerComponents })

Passing Instances Through Your Own Context

createTableHookContexts is also useful on its own, separately from createTableHook, for avoiding prop drilling. The column, row, cell, and header instances are stable references (the same stability TanStack Table relies on for React Compiler memoization). You can place any of them on a context and read it deep in a subtree, and because the reference does not change when table state changes, the context value stays the same and consumers will not re-render from state changes.

Reach for createTableHookContexts instead of hand-rolling createContext: it hands you a context plus a matching TFeatures-typed hook, so you do not have to retype the value or write the useContext guard yourself. (See Scoped Contexts With createTableHookContexts below for the other use of this util, passing the contexts into createTableHook.)

tsx
// cell-slot-context.ts
export const { cellContext, useCellContext } =
  createTableHookContexts<typeof features>()

// provide the (stable) cell once, near where you render it
function CellSlot({ cell }: { cell: Cell<typeof features, Person, unknown> }) {
  return (
    <cellContext.Provider value={cell}>
      <DeeplyNestedCellUI />
    </cellContext.Provider>
  )
}

// read it anywhere below, no prop drilling
function DeeplyNestedCellUI() {
  const cell = useCellContext() // typed with your TFeatures
  return <span>{/* ... */}</span>
}

Important

That stability is exactly why state-dependent method reads go stale. A component that reads something like header.column.getIsSorted(), cell.row.getIsSelected(), or cell.getValue() off a context-provided instance will not re-render when that state changes on its own. Wrap reactive reads in Subscribe (or useSelector) so they re-run on the state they depend on. See Subscribe for React Compiler Compatibility.

The table object is the exception. useTable returns a fresh table reference on every state change, which is intentional so that React Compiler invalidates JSX that depends on it. It is therefore not a stable context value: providing the table through your own context re-renders consumers whenever the providing component re-renders. If you want a stable handle to pass down, provide table.store or a specific atom such as table.atoms.rowSelection (these are stable) and read them with Subscribe / useSelector, or keep the table in a ref.

The Default Shared Context

By default you do not create or wire any context yourself. createTableHook connects its AppTable/AppCell/AppHeader providers to a shared, module-scoped context, and the hooks it returns read from that same context. Because the context lives at module scope, its identity is stable across hot module replacement during local development, so a component edit never leaves a provider and a consumer pointing at mismatched contexts.

Independent tables stay isolated through normal provider scoping: each <table.AppTable> provides its own table instance, and a consumer reads the nearest provider. Rendering two different tables side by side, or in different routes, works without any extra setup.

Scoped Contexts With createTableHookContexts

The shared context only becomes a problem if you nest one table's provider inside another's and a consumer in that overlap reads the nearest (inner) provider instead of the one you intended. For that case, create an isolated context with createTableHookContexts and pass it into createTableHook:

tsx
// scoped-table-context.ts (no component imports)
import { createTableHookContexts, tableFeatures } from '@tanstack/react-table'

export const features = tableFeatures({ /* ... */ })

export const {
  tableContext,
  cellContext,
  headerContext,
  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHookContexts<typeof features>()
tsx
// table.ts
import { createTableHook } from '@tanstack/react-table'
import { cellContext, features, headerContext, tableContext } from './scoped-table-context'

export const { useAppTable } = createTableHook({
  features,
  tableContext, // <- providers now use these scoped contexts
  cellContext,
  headerContext,
  tableComponents: {
    /* ... */
  },
})

Each call to createTableHookContexts returns a brand-new set of contexts, so two table setups that use their own scoped contexts can be nested without reading each other's values.

Which Hooks To Use With Scoped Contexts

createTableHookContexts also returns useTableContext/useCellContext/useHeaderContext, but they are typed with TFeatures only. They do not know your registered component maps, because those are defined later in createTableHook. So:

  • Prefer the hooks returned from your createTableHook call for the richest types (your tableComponents/cellComponents/headerComponents are typed).

  • Use the hooks from createTableHookContexts only when you need to read context from a module that cannot import the createTableHook result.

    This mirrors TanStack Form's createFormHookContexts, where the context hooks are intentionally loose and the strongly typed path lives on the form/field components.

When To Use This

  • Do nothing special for the common case. The default shared context handles single tables and multiple independent tables.
  • Reach for createTableHookContexts only when you nest different table setups and need their contexts isolated, or when you want to read context from a component-free module.