Skip to content

createTableHook composable table example can break during Vite HMR because tableComponents creates a circular import with useTableContext #6348

Description

@kumpelstachu

TanStack Table version

v9.0.0-beta.17

Framework/Library version

React v19.2.7

Describe the bug and the steps to reproduce it

When using the new composable tables API with createTableHook and registered tableComponents, a component can throw during Vite HMR even though it is rendered inside <table.AppTable>.

The runtime error is:

`useTableContext` must be used within an `AppTable` component. Make sure your component is wrapped with `<table.AppTable>...</table.AppTable>`.

This happens with the same structure shown/recommended by the composable tables example: the hook module creates the table hook and imports registered table components, while those table components import useTableContext from the hook module.

This creates a circular dependency:

hooks.ts
  imports TableComponent from table-components.tsx

table-components.tsx
  imports useTableContext from hooks.ts

The app works after a full reload, but after HMR/Fast Refresh the registered component may read from a different/stale context identity than the one provided by <table.AppTable>.

Minimal reproduction

// hooks.ts
import { createTableHook } from '@tanstack/react-table'
import { TableComponent } from './table-components'

export const {
  useAppTable,
  useTableContext,
} = createTableHook({
  features,
  tableComponents: {
    TableComponent,
  },
})
// table-components.tsx
import { useTableContext } from './hooks'

export function TableComponent() {
  const table = useTableContext()

  return <div />
}
// app.tsx
import { useAppTable } from './hooks'

function App() {
  const table = useAppTable({
    // table options
  })

  return (
    <table.AppTable>
      <table.TableComponent />
    </table.AppTable>
  )
}

Steps to reproduce

  1. Create a Vite React app.
  2. Use @tanstack/react-table@beta.
  3. Set up createTableHook with tableComponents.
  4. In a registered component, call useTableContext() imported from the same hook module that imports the component.
  5. Render the component inside <table.AppTable>.
  6. Edit/save either the registered component file or the hook file so that Vite HMR runs.
  7. Observe that useTableContext() may throw even though the component is still rendered inside <table.AppTable>.

Expected behavior

Registered table components should be safe to use with HMR when following the documented/recommended pattern.

This should work reliably:

<table.AppTable>
  <table.TableComponent />
</table.AppTable>

and inside TableComponent:

const table = useTableContext()

Actual behavior

After HMR, useTableContext() sometimes throws as if there was no provider:

useTableContext must be used within an AppTable component.

A full page reload fixes the issue.

Why this is hard to avoid with the current API

The current API returns useTableContext from createTableHook:

export const {
  useAppTable,
  useTableContext,
} = createTableHook({
  features,
  tableComponents,
})

But tableComponents are often defined in a separate module, and the docs/comments suggest using useTableContext() inside those components.

That naturally leads to this circular dependency:

hooks.ts -> table-components.tsx -> hooks.ts

Suggested API

TanStack Form already solves a very similar problem by separating context creation from hook creation:

// context.ts
export const {
  fieldContext,
  formContext,
  useFieldContext,
  useFormContext,
} = createFormHookContexts()
// hooks.ts
export const {
  useAppForm,
} = createFormHook({
  fieldComponents,
  formComponents,
  fieldContext,
  formContext,
})

It would be very helpful if TanStack Table had a similar API, for example:

// table-features.ts
export const features = tableFeatures({ ... })
// table-context.ts
import { createTableHookContexts } from '@tanstack/react-table'
import { features } from './table-features'

export const {
  tableContext,
  cellContext,
  headerContext,

  useTableContext,
  useCellContext,
  useHeaderContext,
} = createTableHookContexts({
  features,
})
// table-components.tsx
import { useTableContext } from './table-context'

export function TableComponent() {
  const table = useTableContext()

  return <div />
}
// hooks.ts
import { createTableHook } from '@tanstack/react-table'
import { features } from './table-features'
import { tableContext, cellContext, headerContext } from './table-context'
import { TableComponent } from './table-components'

export const {
  useAppTable,
  createAppColumnHelper,
} = createTableHook({
  features,
  tableComponents: {
    TableComponent,
  },

  tableContext,
  cellContext,
  headerContext,
})

This would avoid the circular import entirely:

hooks.ts -> table-components.tsx -> table-context.ts
hooks.ts -> table-context.ts
hooks.ts -> table-features.ts

instead of:

hooks.ts -> table-components.tsx -> hooks.ts

The important part is that useTableContext, useCellContext, useHeaderContext, etc. would still be typed according to the same TFeatures used by createTableHook.

The exact API shape may differ, but an official HMR-safe pattern that preserves feature-specific context hook types would be very useful.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

https://stackblitz.com/github/tanstack/table/tree/beta/examples/react/composable-tables

Screenshots or Videos (Optional)

No response

Do you intend to try to help solve this bug with your own PR?

None

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions