Skip to content

[Bug] table-grouped template renders <tr> directly inside <table> — invalid DOM, and a hydration error for anyone who uses the template as a page #5277

Description

@rubyycheung

Summary

The table-grouped page template renders <TableRow> as a direct child of
<Table>, with no <TableBody> between them. The emitted DOM is
<table><tr>…, which is invalid HTML. React's DOM nesting validation flags it
on every render:

In HTML, <tr> cannot be a child of <table>. Add a <tbody>, <thead> or <tfoot>
to your code to match the DOM tree generated by the browser.
This will cause a hydration error.

<table className="astryx-base-table …" style={{minWidth:"596px"}}>
  <colgroup>
  <TableRow role="button" tabIndex={0} …>
>   <tr role="button" tabIndex={0} className="astryx-table-row …">

It is the only one of the 31 page templates with this shape. Every other
template that renders a Table uses the data-driven data={…} API, where
BaseTable supplies the <tbody> itself.

Why it matters more than a console warning

A page template is starter code — the documented use is to copy it into an app
as a page. In a Next.js App Router page that is server-rendered, and there the
warning becomes a real failure: the HTML parser applies implied-<tbody>
insertion when it parses the server's markup, so the client tree and the parsed
tree disagree and hydration mismatches. Anyone who adopts this template inherits
that, in their app, not in ours.

Even client-only, the DOM is invalid: rows created through appendChild are not
reparented, so the table ends up with <tr> children and no <tbody> at all.
Any CSS or query that targets tbody — a consumer's own, or a future one in
core — silently misses.

Root cause

Table's children mode used to wrap its children in a <tbody> for you.
#2097 changed that, and its own text records the old behaviour:

Today, children is dumped into a single <tbody> — after this change,
children can include XDSTableHeader, XDSTableBody, and XDSTableFooter
for full structural control.

BaseTable now renders {children ? children : <>…</>}, so children go in
raw. That is the right API. But the template was written against the implicit
wrapper and was never migrated, and nothing caught it:

  • No runtime or build-time diagnostic — Table accepts any children.
  • The changelog entry for the feature (feat(Table): structural children mode + XDSTable in Markdown #2098, "Table structural children mode")
    reads as an addition; there is no note that children stopped being wrapped.
  • Table.doc.mjs still describes the prop as "Children mode: render
    TableRow/TableCell directly instead of using data-driven rendering"
    , and its
    related-components list names TableRow, TableCell, and TableHeaderCell
    — not TableHeader, TableBody, or TableFooter. A reader following the
    component's own documentation writes exactly what this template contains.

So the template is the visible instance, but the documentation still teaches
the shape that produces it.

Fix

Three lines in assets/templates/pages/table-grouped/page.tsx — import
TableBody, and wrap the groupKeys.map(…) block in it, after the
<colgroup>:

   TableRow,
   TableCell,
+  TableBody,
   proportional,
               </colgroup>
+              <TableBody>
               {groupKeys.map(key => {
                 …
               })}
+              </TableBody>
             </Table>

TableBody is already exported from @astryxdesign/core/Table, and it is the
same <tbody> the data-driven path renders, so nothing about the styling,
dividers, or column widths changes. Verified against 0.4.3: type-checks clean,
and the nesting error goes away.

Suggestions beyond the template

  1. Say it in the docs. Table's children description and its
    related-components list should name TableHeader / TableBody /
    TableFooter, and one children-mode example should show the wrapper. As it
    stands the docs describe the pre-feat(Table): make BaseTable private, add children-mode structural sub-components #2097 contract.
  2. Consider warning in dev. Table in children mode could check for a
    direct TableRow child and warn, the way astryx theme build already warns
    about a theme naming a font it never loads — another case where the output
    is well-formed and simply does not do what the author meant.
  3. Worth a sweep. feat(Table): make BaseTable private, add children-mode structural sub-components #2097 was a silent behaviour change to a public API;
    other consumers written before it may carry the same shape.

Versions

@astryxdesign/core@0.4.3, @astryxdesign/cli@0.4.3 (template assets),
React 19, Next.js 16.2.x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:tablesbugSomething isn't workingcomponentNew or improved UI componentdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions