Remix notes
Rough edges in Remix 3.1 found while building Stems, and the workarounds.
Bugs and rough edges in Remix found while building Stems, with minimal reproductions, so they can be
reported to remix-run/remix. Each lists the Stems workaround.
1. Nodes added to a component's array output are appended after the parent's later siblings
Version: remix@3.1.0 (@remix-run/component@1.1.0). Confirmed with the test below
(Nov 1, 2026): the first case passes, the second fails.
When a child component returns an array and that array grows on a re-render, the new DOM nodes are appended at the end of the shared parent instead of after the component's existing nodes. An array mapped directly in the owner's JSX is fine.
import * as assert from 'remix/assert'
import { describe, it } from 'remix/test'
import type { Handle } from 'remix/component'
import { render } from 'remix/component/test'
it('keeps order when a child component returns a growing array', async (t) => {
let items = ['a']
let owner: Handle | undefined
function Chips(handle: Handle<{ items: string[] }>) {
return () => handle.props.items.map((item) => <span key={item}>{item}</span>)
}
function App(handle: Handle) {
owner = handle
return () => (
<div id="root">
<Chips items={items} />
<input />
</div>
)
}
let { $, act, cleanup } = render(<App />)
t.after(cleanup)
items = ['a', 'b']
await act(() => owner!.update())
let order = [...$('#root')!.childNodes].map((n) => (n as Element).tagName + ':' + n.textContent)
// Actual: ['SPAN:a', 'INPUT:', 'SPAN:b']
assert.deepEqual(order, ['SPAN:a', 'SPAN:b', 'INPUT:'])
})
Workaround in Stems: fixChipOrder() in packages/stems-ui/src/lib/combobox-core.ts moves
chips back before the input after each render. Apps can avoid it by wrapping array output in an
element.
2. Mixins called without arguments receive the host props as their first argument
createMixin runners are called as (...args, props), so foo() puts the props in the
options slot. Not a bug per se, but easy to get wrong (@remix-run/ui's own toggle() would
crash on a no-argument call). Workaround: splitOptions in src/lib/collapsible-panel.ts.
3. Boolean aria-* values render differently on the server and the client
The server renderer writes true as a bare attribute and drops false; the client writes
"true"/"false". Workaround: always pass aria values as strings.
4. A mixin's own handle.update() can re-render with stale arguments
After the owning component re-rendered, handle.update() from inside a mixin re-ran it with the
previous arguments/props (reverted a controlled input value). Reproduced by one agent, not by
another; worth a minimal repro before reporting. Workaround: re-render through the Context
provider's handle.
5. attrs({ value: undefined }) clears inputs
attrs() copies keys whose value is undefined, and an explicit value: undefined makes the
reconciler clear the input's value, which wipes uncontrolled defaults. Workaround: only add the
key when it's defined.
6. Server-rendered <style> tags have no CSP nonce
Blocks a real CSP Provider (see docs/audit.md). No workaround yet.
7. remix test browser harness must live under the cwd
With pnpm the harness files are in the repo-root store, so package-level remix test fails with
"Harness scripts … are outside rootDir". Workaround: run tests from the repo root.
8. remix test browser iframes drift below the fold, which stalls WebKit
remix test runs each browser test file in an iframe appended to the runner page, under a
results list that grows as files finish. Finished iframes are hidden, but later ones end up
below the viewport. WebKit pauses requestAnimationFrame in offscreen iframes, so any test that
waits for a frame stalls until the runner's 90s "no progress" timeout (in our suite, the 22nd
file). Chromium and Firefox don't throttle same-origin iframes this way. Workaround: a very
tall viewport in playwright.cross.config.ts. A fix upstream could keep the active iframe
in view (e.g. position: fixed).