Components
Components let you split your UI into reusable, self-contained pieces. A component is a function decorated with @ps.component that returns Pulse elements.
Why components?
As your application grows, you'll find yourself repeating similar UI patterns. Components solve this by letting you:
- Reuse UI - Define a pattern once, use it many times
- Encapsulate state - Each component instance can have its own internal state
- Organize code - Break complex UIs into manageable pieces
Creating a component
Add the @ps.component decorator to any function that returns Pulse elements:
@ps.component
def Card(title: str, *children, key=None):
return ps.section(className="border rounded-lg p-4 shadow")[
ps.h3(title, className="text-lg font-bold mb-2"),
*children,
]Then use it like any other element:
def Page():
return ps.div()[
Card(title="Welcome")[
ps.p("Hello, world!"),
],
Card(title="Features")[
ps.ul()[
ps.li("Fast"),
ps.li("Simple"),
ps.li("Powerful"),
],
],
]Component props
Components accept arguments just like regular Python functions:
@ps.component
def Button(label: str, variant: str = "primary", onClick=None, key=None):
class_map = {
"primary": "bg-blue-500 text-white",
"secondary": "bg-gray-200 text-gray-800",
}
return ps.button(
label,
className=f"px-4 py-2 rounded {class_map.get(variant, '')}",
onClick=onClick,
)Accepting children
To allow your component to wrap other elements, use *children as a parameter:
@ps.component
def Container(*children, key=None, className: str = ""):
return ps.div(className=f"max-w-4xl mx-auto p-6 {className}")[
*children
]The *children parameter enables the bracket syntax for passing nested content:
Container(className="bg-gray-100")[
ps.h1("Page Title"),
ps.p("Page content..."),
]Components with state
Components can have their own internal state using hooks. This is covered in detail in the State and Hooks guides:
@ps.component
def Counter(key=None):
with ps.init():
state = CounterState()
return ps.div()[
ps.button("-", onClick=state.decrement),
ps.span(f"{state.count}"),
ps.button("+", onClick=state.increment),
]Each instance of Counter maintains its own independent count.
Rendering lists with ps.For
Use ps.For to render a list of items:
@ps.component
def TodoList():
with ps.init():
state = TodosState()
return ps.div(
ps.For(state.todos, lambda todo: TodoItem(todo, key=todo.id))
)ps.For takes an iterable and a function that returns a Pulse element for each item.
Why use ps.For?
Python's late binding semantics can cause subtle bugs with list comprehensions:
# Problematic - all callbacks reference the last item!
[ps.button(f"Delete {item.name}", onClick=lambda: delete(item))
for item in items]
# Safe - ps.For handles binding correctly
ps.For(items, lambda item: ps.button(
f"Delete {item.name}",
onClick=lambda: delete(item)
))In the first example, all onClick handlers end up deleting the same item (the last one) because item is captured by reference, not by value.
Keys preserve component identity
Keys tell Pulse which component is which when items move, get added, or get removed. Without keys, Pulse identifies components by their position in the list.
@ps.component
def ListItem(text: str, key=None):
with ps.init():
state = ItemState() # Each item has its own state
return ps.div(
ps.input(value=state.value, onChange=...),
ps.span(text),
)
@ps.component
def ItemList():
items = ["Apple", "Banana", "Cherry"]
return ps.div(
# With keys - state follows the item
ps.For(items, lambda item: ListItem(item, key=item)),
)When you add a new item at the beginning of the list:
- With keys: Existing items keep their state (input values, checkbox states, etc.)
- Without keys: State stays in position, so items appear to lose their state
Key requirements
Keys must be:
- Stable: The same item should always have the same key
- Unique: No two items in the same list should share a key
Good keys:
key=item.id # Database IDs
key=item.uuid # UUIDs
key=f"{item.type}:{item.id}" # Composite keysAvoid using array indices as keys unless the list never reorders:
# Only OK if items never move or get inserted
ps.For(enumerate(items), lambda i, item: Component(item, key=i))Component keys
Keys are specified as a keyword argument on components. By convention, components that might be used in lists should accept an optional key parameter:
@ps.component
def Card(title: str, key=None):
# key=None makes it optional
return ps.div(ps.h3(title))Keys only work within a single level of the tree. They help Pulse track items within a list, but cannot be used to move a component to a completely different part of the UI.
Iterables as children
You can also pass iterables directly as children to any element:
def Navigation():
links = [("Home", "/"), ("About", "/about"), ("Contact", "/contact")]
return ps.nav()[
[ps.a(text, href=url) for text, url in links]
]Pulse automatically flattens nested iterables. If the iterable contains components, Pulse will warn you if they don't have keys.