Pulse

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 keys

Avoid 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.

See also

On this page