1. The view tree¶
The basic unit of tempestweb is the view() function. It receives the app
(which exposes the current state) and returns a widget tree. No JSX, no
template — just plain, typed Python.
The view function¶
from tempest_core import App, Column, Style, Text, Widget
from tempest_core.style import Edge
def view(app: App[CounterState]) -> Widget: # (1)!
"""Render the counter UI from the current state."""
return Column( # (2)!
style=Style(gap=8.0, padding=Edge.all(16)),
children=[
Text(content=f"Count: {app.state.value}", key="label"), # (3)!
],
)
viewreceivesApp[CounterState]— the typed state handle — and always returns aWidget. Input and output types are part of the contract.Columnis a vertical flex container.Rowis the horizontal one. Both takestyleandchildren.Textshows text.app.state.valuereads the current state — the view is a function of state.
The view is pure
view() mutates nothing. It reads app.state and describes the UI that
matches that state. Changing the state is the handlers' job (next page) — the
view only draws.
The counter's widgets¶
The counter uses four widget types, all from the core:
| Widget | What it is | Main props |
|---|---|---|
Column |
Vertical flex container | style, children |
Row |
Horizontal flex container | style, children |
Text |
Text | content, style, key |
Button |
Clickable button | label, on_click, style, key |
The key: stable identity¶
Notice key="label". The key gives the widget a stable identity across
rebuilds. When the state changes and the view runs again, the reconciler uses the
key to match the new widget with the old one — so it can emit a minimal patch
(change only the text) instead of recreating the node.
When to give a key
Give a key to any widget that persists across rebuilds and whose content
changes (the count text, the buttons). Dynamic list items also want a stable
key. Without a key, reconciliation falls back to positional matching.
Style is a typed object¶
Style is a Pydantic object — not a CSS string. You declare intent and the
client translates it to CSS:
gap=8.0→gap: 8pxon the flex container.Edge.all(16)→padding: 16px 16px 16px 16px.
Style → CSS is almost identity
Style was designed by copying the CSS vocabulary (flexbox, box model,
typography). The translation lives in the client (client/style.js) and is
shared by both modes. Full detail in the wire contract.
The complete counter tree¶
Putting container, text and buttons together:
from tempest_core import App, Button, Column, Row, Style, Text, Widget
from tempest_core.style import Edge
def view(app: App[CounterState]) -> Widget:
"""Render the counter UI from the current state."""
return Column(
style=Style(gap=8.0, padding=Edge.all(16)),
children=[
Text(content=f"Count: {app.state.value}", key="label"),
Row(
style=Style(gap=4.0),
children=[
Button(label="-", key="dec"), # on_click comes on page 2
Button(label="+", key="inc"),
],
),
],
)
This produces the tree (IR) that the reconciler serializes to the client — the
exact format is pinned in
tests/fixtures/node_initial.json.
Recap¶
view(app) -> Widgetis a pure function of state.- Widgets (
Column,Row,Text,Button) are typed core objects. keygives stable identity so reconciliation emits minimal patches.Styleis a typed object that becomes CSS in the client.
Now the buttons need to do something. Let's move on to state and handlers. 🚀