Skip to content

rx.code_block renders empty when content is a state Var (0.9.2) #6480

@jryberg

Description

@jryberg

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:583rollupOptions.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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions