s-table-header-row warns « Provide a s-table-header with a listSlot of ‘primary’ » on every React-mounted table, even though the primary header is there
I’m using polaris web components from https://cdn.shopify.com/shopifycloud/polaris.js (CDN build of 2026-09-21), React 18.3.1, Chrome 153, embedded app in the Shopify admin. Same result on a standalone page outside the admin.
What we see. Every s-table our React app mounts logs this once in the console:
polaris: <s-table-header-row> - Provide a s-table-header with a listSlot of 'primary'
Each of those tables has exactly one s-table-header with listSlot="primary", one secondary, and the rest inline or labeled. The table renders correctly right after the warning. Only the console is wrong, but it fires on 16 tables across our admin, so it drowns out real warnings.
Steps to reproduce. One page, no build step. The vanilla-DOM table is silent, the React table warns.
<script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.js"></script>
<div id="vanilla"></div>
<div id="react"></div>
<script>
// Vanilla: createElement + setAttribute, appended bottom-up, then inserted. No warning.
const mk = (tag, attrs, kids) => {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
for (const k of kids) el.append(k);
return el;
};
document.getElementById('vanilla').append(
mk('s-table', {}, [
mk('s-table-header-row', {}, [
mk('s-table-header', { listSlot: 'primary' }, ['Name']),
mk('s-table-header', { listSlot: 'secondary' }, ['Email']),
mk('s-table-header', { listSlot: 'inline' }, ['Status']),
]),
mk('s-table-body', {}, [mk('s-table-row', {}, [mk('s-table-cell', {}, ['A']), mk('s-table-cell', {}, ['a@x']), mk('s-table-cell', {}, ['ok'])])]),
]),
);
// React 18: same tree, same attributes. Warns once.
const h = React.createElement;
ReactDOM.createRoot(document.getElementById('react')).render(
h('s-table', null,
h('s-table-header-row', null,
h('s-table-header', { listSlot: 'primary' }, 'Name'),
h('s-table-header', { listSlot: 'secondary' }, 'Email'),
h('s-table-header', { listSlot: 'inline' }, 'Status')),
h('s-table-body', null,
h('s-table-row', null, h('s-table-cell', null, 'A'), h('s-table-cell', null, 'a@x'), h('s-table-cell', null, 'ok')))),
);
</script>
Why I think it happens (traced in the bundle). The Polaris base element class overrides setAttribute. While an element is not yet connected, a setAttribute call whose name is a key of the element’s React props (__reactProps$…) is dropped, and Polaris re-applies those props itself in the element’s connectedCallback. That is fine on its own, but connectedCallback runs parent-first. So when React inserts a finished table:
s-table-header-rowconnects, renders, and readslistSloton each child. The children still return thelabeleddefault because their attribute write was dropped.- The row finds no
primaryand warns. - Each
s-table-headerconnects, gets its props applied, dispatches the attribute-change event, and the row re-renders correctly.
With the listslot attribute spelled lowercase it is the same. The vanilla build has no React props on the element, so nothing is swallowed and the row reads the right value the first time.
What we expect. A table whose header row contains one listSlot="primary" header should not warn, whatever framework inserted it. Two ways this could be fixed on the Polaris side:
- Apply a disconnected element’s React props when they are first seen, before the parent’s
connectedCallbackreads the children, or - Have
s-table-header-rowdefer itslistSlotvalidation until its slotted headers have connected, since the same code already re-renders on their attribute-change event.
We would rather not carry a framework-specific workaround (mounting the headers one commit after the row does silence it) for what the docs present as plain markup.
Is this a known issue, and is a fix planned?