Describe the bug
rx.code_block(state_var, language="yaml", show_line_numbers=True) renders an empty <pre><code class="language-yaml"> element when the first positional argument is a state Var. The Var is populated at runtime (other components reading the same Var work — e.g. rx.set_clipboard(state_var) copies the full content, and rx.cond(state_var != "", ...) evaluates True) — but the content never reaches SyntaxHighlighter.
The same call with a literal Python string in the same position renders correctly.
rx.markdown(<expression containing state_var>, component_map={"pre": ...}) — the documented v0.9.0 replacement for codeblock in component_map — also renders no <pre> at all in this configuration, so the markdown route is not a workaround.
Versions
reflex 0.9.2
reflex-components-code 0.9.2
reflex-components-markdown 0.9.1
- Python 3.13 (container) / 3.12 (local) — same result on both
- React 19, react-router 7, Vite 8
Minimal reproduction
import reflex as rx
class State(rx.State):
content: str = (
"version: v3\n"
"teleport:\n"
" nodename: example\n"
" auth_token: abcd1234\n"
)
@rx.page()
def index() -> rx.Component:
return rx.vstack(
rx.heading("Literal string (renders correctly):", size="3"),
rx.code_block(
"version: v3\nteleport:\n nodename: example\n auth_token: abcd1234\n",
language="yaml",
show_line_numbers=True,
),
rx.heading("State Var (renders empty):", size="3"),
rx.code_block(
State.content,
language="yaml",
show_line_numbers=True,
),
spacing="4",
padding="4",
)
app = rx.App()
Expected behavior
The second rx.code_block renders the same YAML content as the first, syntax-highlighted with line numbers.
Actual behavior
The second <pre> element renders, but its <code class="language-yaml"> child has zero text content:
<pre class="css-..."><code class="language-yaml" style="..."></code></pre>
Root cause
The auto-memoized wrapper compiled for rx.code_block(state_var, ...) (.web/utils/components/Codeblock_prismasynclight_<hash>.jsx) reads the state Var via useContext and passes it via props.children — but also passes the destructured wrapper-children as the third positional argument to @emotion/react's jsx():
import { memo, useContext } from "react"
import { PrismAsyncLight as SyntaxHighlighter } from "react-syntax-highlighter"
import { StateContexts } from "$/utils/context"
import { jsx } from "@emotion/react"
export const Codeblock_prismasynclight_<hash> = memo(({children}) => {
const reflex___state__... = useContext(StateContexts.reflex___state__...)
return(
jsx(SyntaxHighlighter,
{children: reflex___state__....content_rx_state_, css: ..., language: "yaml", showLineNumbers: true, style: ...},
children // <-- destructured from wrapper props; undefined when wrapper is invoked with no children
)
)
});
@emotion/react's jsx (in the production bundle):
function jsx(e, t) {
var n = arguments;
if (t == null || !hasOwnProperty.call(t, "css")) {
return React.createElement.apply(void 0, n); // <-- both paths spread all 3 args
}
// ...wraps with emotion css component, still spreads all args...
}
React.createElement(type, props, undefined) has arguments.length === 3, so it sets props.children = undefined, overriding the children value set in the props object. SyntaxHighlighter then receives children: undefined and renders the language-class wrapper but no highlighted content.
Source pointer
The unconditional positional spread happens in reflex_base/components/component.py:2617-2623 (v0.9.2):
return FunctionStringVar.create(
"jsx",
).call(
tag_name,
props,
*[render_dict_to_var(child) for child in tag["children"]], # spreads even when empty
)
When tag["children"] is empty the spread is empty, but the auto-memoized wrapper for a stateful subtree (e.g. Codeblock_prismasynclight_<hash> above) still introduces a destructured children parameter that gets passed positionally as the third arg, producing jsx(SyntaxHighlighter, props, undefined). The undefined is enough to override props.children.
reflex_components_code/code.py:502-514 is where the state Var ends up in props["children"]:
def _render(self):
out = super()._render()
theme = self.theme
return (
out
.add_props(style=theme)
.remove_props("theme", "code")
.add_props(children=self.code) # <-- value goes into props.children
)
Workaround
rx.box(rx.text(state_var, font_family="mono", style={"white-space": "pre-wrap"})). Loses syntax highlighting but content is visible.
Observed but separate
On every build, Vite 8 logs:
Warning: Invalid input options (1 issue found) - For the "jsx". Invalid key: Expected never but received "jsx".
This is the rollupOptions: { jsx: {} } line in reflex_base/compiler/templates.py:583 — rollupOptions.jsx is no longer accepted by Vite 8. This is a build-config compatibility issue separate from the rendering bug above (the rendering bug reproduces regardless of the warning since the .jsx files use explicit jsx() calls, not JSX syntax).
Describe the bug
rx.code_block(state_var, language="yaml", show_line_numbers=True)renders an empty<pre><code class="language-yaml">element when the first positional argument is a stateVar. The Var is populated at runtime (other components reading the same Var work — e.g.rx.set_clipboard(state_var)copies the full content, andrx.cond(state_var != "", ...)evaluatesTrue) — but the content never reachesSyntaxHighlighter.The same call with a literal Python string in the same position renders correctly.
rx.markdown(<expression containing state_var>, component_map={"pre": ...})— the documented v0.9.0 replacement forcodeblockincomponent_map— also renders no<pre>at all in this configuration, so the markdown route is not a workaround.Versions
reflex0.9.2reflex-components-code0.9.2reflex-components-markdown0.9.1Minimal reproduction
Expected behavior
The second
rx.code_blockrenders the same YAML content as the first, syntax-highlighted with line numbers.Actual behavior
The second
<pre>element renders, but its<code class="language-yaml">child has zero text content:Root cause
The auto-memoized wrapper compiled for
rx.code_block(state_var, ...)(.web/utils/components/Codeblock_prismasynclight_<hash>.jsx) reads the state Var viauseContextand passes it viaprops.children— but also passes the destructured wrapper-children as the third positional argument to@emotion/react'sjsx():@emotion/react'sjsx(in the production bundle):React.createElement(type, props, undefined)hasarguments.length === 3, so it setsprops.children = undefined, overriding the children value set in the props object.SyntaxHighlighterthen receiveschildren: undefinedand renders the language-class wrapper but no highlighted content.Source pointer
The unconditional positional spread happens in
reflex_base/components/component.py:2617-2623(v0.9.2):When
tag["children"]is empty the spread is empty, but the auto-memoized wrapper for a stateful subtree (e.g.Codeblock_prismasynclight_<hash>above) still introduces a destructuredchildrenparameter that gets passed positionally as the third arg, producingjsx(SyntaxHighlighter, props, undefined). The undefined is enough to overrideprops.children.reflex_components_code/code.py:502-514is where the state Var ends up inprops["children"]:Workaround
rx.box(rx.text(state_var, font_family="mono", style={"white-space": "pre-wrap"})). Loses syntax highlighting but content is visible.Observed but separate
On every build, Vite 8 logs:
This is the
rollupOptions: { jsx: {} }line inreflex_base/compiler/templates.py:583—rollupOptions.jsxis no longer accepted by Vite 8. This is a build-config compatibility issue separate from the rendering bug above (the rendering bug reproduces regardless of the warning since the .jsx files use explicitjsx()calls, not JSX syntax).