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 | |
1 | |
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 |
Related¶
- Usage — the tag API itself: naming rules, custom tags, SVG
- Responding to Events — the event
dict,asynchandlers - Rendering Lists — keys and why rows keep the wrong state
- Use Ref — ReactPy's
Ref, which is never attached to an element