Skip to content

Common Props

Overview

Every element a ReactPy component renders is a plain dict with a fixed set of keys, and the attributes you pass to a tag end up on the DOM node nearly untouched. This page is the reference for what you may put in there — and for the handful of places ReactPy behaves differently from React.

{
    "tagName": "div",
    "children": [...],
    "attributes": {...},
    "eventHandlers": {...},
    "inlineJavaScript": {...},
    "importSource": {...},
}

Only "tagName" is required. You almost never write this by hand — reactpy.html builds it for you — but everything below is what a tag call can carry.


Attributes

An attribute is any key/value pair in the first argument to a tag:

html.input({"type": "search", "placeholder": "Filter", "autoFocus": True})

ReactPy forwards the name and the value to the browser verbatim. There is no translation layer, which is the main difference from React, where JSX rewrites props before they reach the DOM.

The value must be JSON-serializable, because the model is sent to the browser as JSON. Nothing stops you from putting something else in there: with debug mode off, the value is accepted at the tag call and only fails when the layout tries to send the model, deep inside the connection:

TypeError: Type is not JSON serializable: object

Run with REACTPY_DEBUG=1 and the same mistake fails at the tag call, where you can see which element caused it:

TypeError: Object of type object is not JSON serializable

Write the name the browser expects

Attribute names are not validated against any allow-list. If the browser understands an attribute, ReactPy delivers it — including data-*, aria-*, and attributes ReactPy has never heard of:

html.div({"aria-live": "polite", "data-row-id": 7, "spellcheck": False})

class and className

Both work, and both produce class="..." in the DOM:

html.div({"class": "one two"}, "...")
html.div({"className": "one two"}, "...")

There is no JSX in Python, so nothing forces you toward className. Pick one and stay consistent — class reads better beside the HTML it mirrors, className is familiar to anyone arriving from React.

for and htmlFor

Both work on <label>, and both produce for="...". Prefer for; htmlFor exists only for readers who expect it.

style

Pass a dict. Keys may be camelCase or kebab-case — both are accepted, and both reach the browser in a form it understands:

html.div(
    {
        "style": {
            "backgroundColor": "rgb(255, 0, 0)",  # camelCase
            "font-size": "20px",  # kebab-case
        }
    }
)

Always write the unit as part of the string

Do not rely on a bare number being given a unit. React appends px to numeric style values; ReactPy's executors do not agree with each other on this. Measured in Chrome, the same component rendered through each executor:

style value ReactPy / ReactPyMiddleware ReactPyCsr
{"fontSize": "30px"} font-size: 30px font-size: 30px
{"font-size": "30px"} font-size: 30px font-size: 30px
{"fontSize": 30} font-size: 30px nothing at all
{"fontSize": "30"} nothing nothing
{"zIndex": 5} z-index: 5 z-index: 5
{"opacity": 0.5} opacity: 0.5 opacity: 0.5

A number that needs a unit is dropped in silence — no warning, no log line, and the layout model shows the value untouched, so it is nowhere to be found by reading the server side. Unitless properties like z-index and opacity are the exception: a number is already a valid value there.

The portable rule: put units in the string, everywhere.

{"style": {"font-size": 20}}  # works by accident in some executors
{"style": {"font-size": "20px"}}  # works everywhere

All together

A table of rows is where most of these rules meet at once — key in attributes, data-* and aria-* passed straight through, and units inside style strings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ROWS = [
    {"id": 1, "name": "Dana", "role": "admin", "active": True},
    {"id": 2, "name": "Sam", "role": "viewer", "active": False},
]


@component
def row(person):
    # `key` lives in attributes, `data-*` and `aria-*` are forwarded verbatim,
    # and `class` works exactly like `className`. Style values carry their own
    # units, because a bare number is not portable across executors.
    return html.tr(
        {
            "key": person["id"],
            "class": "is-active" if person["active"] else "is-inactive",
            "data-role": person["role"],
            "aria-label": f"{person['name']}, {person['role']}",
            "style": {"opacity": "1" if person["active"] else "0.55"},
        },
        html.td(person["name"]),
        html.td(person["role"]),
        html.td(
            {"style": {"text-align": "right", "font-variant-numeric": "tabular-nums"}},
            "yes" if person["active"] else "no",
        ),
    )


@component
def people_table():
    return html.table(
        None,
        html.thead(
            html.tr(
                None,
                html.th("Name"),
                html.th("Role"),
                html.th({"scope": "col"}, "Active"),
            )
        ),
        # A list of children: every row needs a key or state will follow the
        # row's position rather than the person.
        html.tbody(None, [row(person) for person in ROWS]),
    )
1
# TODO

Rendering this through Layout gives rows whose attributes are exactly what was written:

{
    "tagName": "tr",
    "attributes": {
        "key": 1,
        "class": "is-active",
        "data-role": "admin",
        "aria-label": "Dana, admin",
        "style": {"opacity": "1"},
    },
    "children": [...],
}

Nothing was renamed, and nothing was dropped. In the DOM the row arrives as:

<tr data-role="admin" aria-label="Dana, admin" class="is-active" style="opacity: 1;">

The one attribute that does not reach the DOM is key itself: ReactPy and ReactPyMiddleware consume it for reconciliation and never set it on the element. ReactPyCsr does leak it, so a key="1" attribute shows up in the page — harmless, but do not rely on the attribute being absent or present. Every other name you write arrives as written.

Booleans and numbers

Values are serialized as JSON and coerced by the browser:

You write The browser receives
{"hidden": True} hidden="" — present, therefore true
{"hidden": False} no attribute at all
{"readonly": True} readonly="true"
{"data-x": "1"} data-x="1", readable as element.dataset.x
{"tabindex": 2} tabindex="2"

React's attribute quirks do not carry over. There is no htmlFor requirement, no auto-px, and no special-casing of value versus defaultValue — what you write is what arrives.

dangerouslySetInnerHTML

Injects a raw HTML string, exactly as in React:

html.div({"dangerouslySetInnerHTML": {"__html": "<b>bold</b>"}})

The key is the plain string "dangerouslySetInnerHTML" — not a class to import — and its only field is "__html".

This is the XSS hole

ReactPy does nothing to sanitize the string. Never put user input in it.

A dangerouslySetInnerHTML attribute also suppresses children. Nothing enforces the combination — no error, no warning, at the tag call or at render time — so the children you passed are simply never displayed. Verified in the browser: for html.div({"dangerouslySetInnerHTML": {"__html": "<b>RAW</b>"}}, "CHILDTEXT") the server sends both the attribute and "children": ["CHILDTEXT"], and the rendered element contains only <b>RAW</b>. Pass the children or the raw HTML, never both, because the silent one is your children.


Children

Children are the arguments that follow the attribute dict. ReactPy accepts a wide range of types and normalizes each one:

Child Becomes
str a text node, verbatim
int / float its str(), so 7 renders as "7"
None dropped — renders nothing
True / False the literal text True / False
list / tuple flattened into the children
another dict an element
a @component instance that component's rendered output

Nested lists are flattened, so [items, "more"] is the same as one flat list.

Only None renders nothing

False renders the word False. React-style short-circuits therefore misfire:

html.div(None, items or False)  # renders the word "False" when items is empty
html.div(None, banner if is_visible else None)  # renders nothing

Replace every x and <element> habit with a conditional expression ending in else None.

Void elements

html.br, html.img, html.input, html.hr, html.meta and the rest of the void-element set reject children:

html.br("oops")
TypeError: 'br' nodes cannot have children.

Nothing special is needed for self-closing tags — a tag with no children is simply:

html.img({"src": "/logo.png", "alt": "Logo"})

Fragments

html.fragment groups children without adding an element to the DOM. It renders as an element with an empty tagName, which the client renders as its children:

html.fragment(
    html.h1("Title"),
    html.p("Body"),
)

A fragment accepts key and nothing else:

html.fragment({"id": "x"}, html.p("..."))
TypeError: Fragments cannot have attributes besides 'key'

html(...) — calling html directly — is the same fragment constructor.


Keys

ReactPy matches children between renders to decide what to update versus what to rebuild. It reads the key out of attributes["key"]:

html.ul(
    None,
    [html.li({"key": person["id"]}, person["name"]) for person in people],
)

key is an attribute, not a sibling of tagName

React treats key as a top-level prop. ReactPy reads it from attributes, so this has no effect — the key is silently ignored:

{"tagName": "li", "key": "row-1", "children": ["..."]}

Write {"tagName": "li", "attributes": {"key": "row-1"}, "children": ["..."]}, or simply html.li({"key": "row-1"}, "..."). Passing key= to VdomDict directly raises ValueError: Invalid keys: {'key'}.

Without a key, a child's position in the list becomes its key. That is why reordering a list makes state stick to the wrong row: nothing moved as far as ReactPy is concerned. Keys must be unique among siblings.

When debug mode is on, ReactPy warns for each unkeyed child that came out of a list:

UserWarning: Key not specified for child in list {'children': ..., 'tagName': 'li'}

Run your app with REACTPY_DEBUG=1 to see them — see Rendering Lists for the same warning in context. The check only fires for children inside a list; the direct children of a tag are never warned about, and a generator cannot be inspected at all:

UserWarning: Did not verify key-path integrity of children in generator <generator object ...> - pass a sequence (i.e. list of finite length) in order to verify

Pass a list — a comprehension is usually the smallest change.


Event handlers

Any callable under any attribute name becomes an event handler:

async def save(event):
    await store.save(event["target"]["value"])


html.button({"onClick": save}, "Save")

The name is forwarded as the DOM event to listen for, so the names that work are the DOM event names in camelCase — onClick, onInput, onKeyDown. The handler receives one argument: the event data as a JSON-serializable dict, wrapped in reactpy.types.Event so nested keys are also readable as attributes. There is no "data" key to unwrap — the browser sends a list of arguments, and ReactPy hands you its single element. Make the handler async when the work is slow, because a plain function blocks the client while it runs — see Responding to Events.

A handler wrapped in EventHandler receives the raw list

A plain callable is called with the browser's argument list unpacked, so def f(event) gets the event dict. An EventHandler does the opposite: it is called with the list itself, so def f(data) must index into it. The difference stays invisible until a handler is converted, and then event["target"] becomes TypeError: list indices must be integers or slices, not str:

from reactpy.core.events import EventHandler

def save(event):      # plain callable: receives the event dict
    store.save(event["target"]["value"])


def save_all(data):   # EventHandler: receives the list of arguments
    store.save(data[0]["target"]["value"])


html.button({"onClick": save}, "Save")
html.button({"onClick": EventHandler(save_all, prevent_default=True)}, "Save")

ReactPy's own options

Four behaviours run in the browser before the event is sent to the server. Configure them by wrapping the function, not by an attribute:

from reactpy.core.events import EventHandler

html.form(
    {"onSubmit": EventHandler(submit, prevent_default=True)},
    html.button({"type": "submit"}, "Submit"),
)
Parameter Value Effect
prevent_default bool calls preventDefault() in the browser before sending
stop_propagation bool calls stopPropagation() in the browser before sending
debounce int ms rate-limits value updates on user-input elements
throttle int ms rate-limits outgoing events

A non-negative integer is required for debounce and throttle; anything else is ignored with a browser-console warning rather than an error:

Ignoring invalid debounce value "fast" on event handler "onInput": expected a non-negative integer.

A dict is not an event handler

The React-style {"function": ..., "preventDefault": True} shape does not work here. A dict is not callable, so ReactPy treats it as an ordinary attribute and no listener is ever registered. With debug mode on you get an immediate error instead of silence:

TypeError: Object of type function is not JSON serializable
# wrong: no listener is attached
html.button({"onClick": {"function": save, "preventDefault": True}}, "...")

# right
html.button({"onClick": EventHandler(save, prevent_default=True)}, "...")

Inline JavaScript

A str under an event handler attribute is treated as JavaScript to run in the page instead of being sent to Python:

html.button({"onClick": "console.log(event)"}, "Log the event")

ReactPy records it under the element's inlineJavaScript rather than eventHandlers:

{
    "tagName": "button",
    "children": ["Log the event"],
    "inlineJavaScript": {"onClick": "console.log(event)"},
}

The string is eval'd in the page; if it evaluates to a function, that function is called with the event.

Trusted strings only

This runs with your application's privileges. Never build the string from user input — use a Python handler and pass data through state.

<script> elements reject handlers

html.script raises ValueError: 'script' elements do not support event handlers. It also infers a key from its content or src, so the same script is not re-run on every render.


importSource

importSource marks an element as coming from a JavaScript module rather than a built-in tag. ReactPy's web module support produces it for you; you rarely set it by hand. Its keys are source, fallback, sourceType and unmountBeforeUpdate, and sourceType must be "URL" or "NAME":

JsonSchemaValueException: data.children[0].importSource.sourceType must be one of ['URL', 'NAME']

Invalid dictionaries

ReactPy validates the six key names as it builds an element:

from reactpy.types import VdomDict

VdomDict(tagName="div", bogus=1)
ValueError: Invalid keys: {'bogus'}.
element = VdomDict(tagName="div")
element["bogus"] = 1
KeyError: 'Invalid key: bogus'

A dict used as a child with no "tagName" does not raise. It sends an error to the client, where it appears in the browser console as an error field on the model:

html.div(None, {"children": ["x"]})
ValueError: Expected a VDOM element dict, not {'children': ['x']}

Anything the layout cannot render is reported this way rather than taking the server down: the client receives the error field and the server logs the traceback. Check both when an element renders as nothing.


At a glance

VdomDict keys

Key Type Notes
tagName str required; "" means a fragment
children list any accepted child type, flattened
attributes dict[str, Any] must be JSON-serializable
eventHandlers dict[str, EventHandler] written for you from callables
inlineJavaScript dict[str, str] written for you from str handlers
importSource dict written for you by web modules

Any other key raises, as shown above. VdomDict is a dict subclass, so you can read and update it with normal dict access — but not add a seventh key.

Attribute names worth knowing

Attribute Value
class / className str of space-separated class names
for / htmlFor str, the id of the labelled control
style dict of CSS property to value; include units in strings
dangerouslySetInnerHTML {"__html": str}
key str or int, unique among siblings
anything else forwarded to the browser verbatim
  • Usage — the tag API itself: naming rules, custom tags, SVG
  • Responding to Events — the event dict, async handlers
  • Rendering Lists — keys and why rows keep the wrong state
  • Use Ref — ReactPy's Ref, which is never attached to an element
Authors: Archmonger
Reactive Python and affiliates.