Capítulo 23 de 54

Chapter 23: React Compiler Guide

Core Idea

v9 is the first version compatible with the React Compiler — most code needs zero compiler-specific handling and can drop useMemo/useCallback used only to stabilize useTable inputs, except nested components whose only props are stable Table objects (row, cell, column, header, or the core table), which need an explicit table.Subscribe/Subscribe boundary or they'll silently go stale.

Key Concepts

  • Why the exception exists: row/cell/column/header objects (and the core table reached via row.table) keep a stable reference across state changes — that's normally good, but it means a compiled child receiving one of these as its only prop can get memoized by the compiler even when calling a method on it (row.getIsSelected(), column.getIsSorted(), cell.getValue(), header.column.getCanSort()) would now return something different. The compiler sees "same prop" and reuses the old render.
  • The React-facing useTable return value is different from the core table. The value useTable itself returns updates when its options or selected state change (so JSX written directly in that component updates fine by default). The core table reachable via row.table/cell.table/column.table is the stable-reference one nested components receive — that's the one needing Subscribe.
  • Fix: put table.Subscribe (or the standalone Subscribe import) inside the nested component that reads the changing state, subscribing to the narrowest slice that affects it — e.g. source={row.table.atoms.rowSelection}, selector={(sel) => sel[row.id]} for a single row's checkbox. Wrapping the parent's JSX in Subscribe while the child still only receives the stable row prop does not fix it — the subscription must live where the stateful read actually happens, or the selected value must be threaded down as a prop.
  • For a component depending on several slices (e.g. a <tbody> whose row model depends on filters/sorting/pagination), subscribe to table.store with a selector returning all the slices that can change table.getRowModel()'s result, and call the state-dependent table methods only inside the subscribed render function.
  • Don't blanket-subscribe every cell "just in case" — add a subscription only where a nested component reads state not represented in its props, or where profiling actually shows a stale/slow render.
  • Removing manual memoization: safe once the component compiles successfully, for values whose only purpose was stabilizing useTable inputs (columns, filtered data, callbacks). Table docs/examples still show manual useMemo/useCallback deliberately, so the same snippets stay correct for non-compiled apps — that's not evidence you need them under the compiler. Stable data/columns identity still matters either way (compiler-managed or hand-memoized) — table uses identity to decide when to rebuild columns/row models; unstable references still cause extra recompute or unwanted state resets.
  • Dependency arrays the compiler doesn't touch: any useEffect/useMemo/useCallback you keep must still list every reactive dependency correctly (exhaustive-deps) — the compiler doesn't fix your dependency arrays for you.
  • Why v8 needed 'use no memo': v8's useReactTable had "interior mutability" — the same table object reference, but methods like getRowModel() returned new data as internal values changed. The compiler could cache against the stable outer reference and serve stale results, so v8 components had to opt out of compilation entirely with the 'use no memo' directive. v9's split between a reactive React-facing return value and explicit Subscribe-based access to the stable core table object is the structural fix — no directive needed.

Reference Tables

Benchmark result (10,000 source rows, 100 rendered, 4x CPU throttle, median interaction-to-commit ms, compiler off → on):

InteractionBroad-subscribing ownerFine-grained (Subscribe)
Unrelated parent update−87%−86%
Select one row−68%~unchanged
Sort−11%−8%
Paginate+1%+4%
Replace all data+6%+2%

Takeaway from the maintainers' own benchmark: the compiler's biggest win is stopping unrelated parent re-renders from cascading into the table — Subscribe alone already keeps fine-grained updates fast, so enabling the compiler just for table performance isn't strongly justified; decide based on the whole app.

Key Takeaways

  1. If a nested component's only props are row/cell/column/header/core table, and it calls a state-dependent method on them, it needs its own Subscribe — this is the one compiler gotcha specific to this library.
  2. Subscribe inside the component that reads the state, to the narrowest slice, not around the parent that merely passes the stable object down.
  3. Don't remove memoization "because the compiler is on" without checking it isn't there for another reason (an API needing stable identity, non-compiled fallback support, or a profiled win).

Connects To

  • Table State (React) Guide: the full atom/store/selector model Subscribe builds on.
  • Composable Tables (createTableHook) Guide: patterns for sharing this setup across many tables.