How to Create Custom Components with the `#[component]` Macro in Topcoat
Topcoat transforms async functions into reusable UI components using the #[component] attribute, which automatically implements the Component trait and wires props builders so your functions can be used declaratively inside view! blocks.
Topcoat is an async Rust web framework that turns function definitions into declarative UI components. When you create custom components with the #[component] macro, the system generates the boilerplate needed to integrate with the view system defined in crates/topcoat-view/src/component.rs, letting you focus on rendering logic while the macro handles trait implementations and type conversions.
How the #[component] Macro Works
Under the hood, the #[component] macro—implemented in crates/topcoat-view/macro/src/lib.rs—expands your function into a type that implements the Component trait. This trait requires a Props associated type and a render method that returns a View. The macro automatically generates a props builder, handles the Future wrapping, and registers the component with the view system so it can be instantiated like a native HTML element.
The transformation follows three specific stages:
- Props struct generation – The macro looks for a
Propstype that implements thePropstrait (usually derived with#[derive(Props)]). - Render implementation – The function body is wrapped in a
Futurethat receives a request-scopedCxcontext. - View system integration – The component becomes available inside other
view!blocks with automatic context propagation.
Step 1: Define Props with #[derive(Props)]
Every component starts with a props struct that defines its inputs. Use #[derive(Props)] to generate a builder pattern that the macro will use automatically. Props can include static data, optional callbacks, and special fields like Children.
use topcoat_view::{Props, PropsBuilder};
#[derive(Props)]
pub struct ButtonProps {
pub label: String,
#[prop(optional)]
pub on_click: Option<topcoat_runtime::Handler>,
}
The #[prop(optional)] attribute marks fields that may be omitted during component instantiation. The derive macro creates a ButtonProps::builder() method that the #[component] macro invokes internally when parsing view! syntax.
Step 2: Write the Component Function
Mark an async function with #[component] and accept &Cx as the first argument followed by your props struct. The function must return a View, typically constructed using the view! macro.
use topcoat_core::context::Cx;
use topcoat_view::{view, View};
#[component]
pub async fn Button(cx: &Cx, props: ButtonProps) -> View {
view! {
button
class = "px-4 py-2 rounded bg-blue-600 text-white"
on:click = move |_| {
if let Some(handler) = props.on_click.clone() {
handler.call(cx.clone());
}
}
{ props.label }
}
}
The Cx parameter provides access to the request-scoped context, allowing handlers to interact with the runtime. The macro expands this function into a struct that implements Component, wiring the props builder and creating the async render method required by the trait defined in crates/topcoat-view/src/component.rs.
Step 3: Use Components in view! Blocks
Once defined, components function as first-class citizens within the view system. Invoke them inside view! blocks using XML-like syntax, passing props as attributes.
use topcoat_view::{view, View};
#[component]
pub async fn Page(cx: &Cx) -> View {
view! {
div class = "space-y-4" {
Button {
label: "Click me".into(),
on_click: Some(topcoat_runtime::handler!(|cx| async move {
println!("Button pressed!");
}))
}
Button {
label: "No action".into(),
on_click: None,
}
}
}
}
The view! macro automatically passes the Cx context, builds the props using the generated builder, and awaits the component's render future.
Advanced: Accepting Children with the Children Type
Components that wrap content require a special Children prop. Include Option<Children> in your props struct to accept arbitrary child markup, then render it explicitly within your component logic.
use topcoat_view::{Props, Children};
#[derive(Props)]
pub struct CardProps {
pub title: String,
#[prop(optional)]
pub children: Option<Children>,
}
#[component]
pub async fn Card(cx: &Cx, props: CardProps) -> View {
view! {
div class = "border rounded p-4 shadow" {
h2 { { props.title } }
if let Some(ch) = props.children.clone() {
ch.render(cx).await?;
}
}
}
}
Usage follows block syntax where nested elements become the children:
view! {
Card { title: "Dashboard".into() } {
p { "Welcome to the dashboard!" }
Button { label: "Refresh".into(), on_click: None }
}
}
The Children type implements a render method that takes the Cx context and returns a View, allowing you to control exactly where child content appears in the output.
What the Macro Generates
The #[component] macro expands your annotated function into approximately the following code (simplified for clarity):
pub struct Button;
impl Component for Button {
type Props = ButtonProps;
fn props_builder() -> <Self::Props as Props>::Builder {
ButtonProps::builder()
}
fn render<'cx>(
self,
cx: &'cx Cx,
props: Self::Props
) -> impl Future<Output = Result<View, Error>> + Send {
async move {
// Original function body here
}
}
}
This matches the Component trait signature found in crates/topcoat-view/src/component.rs (lines 5–22), demonstrating how the macro bridges your high-level function definition with the framework's underlying trait requirements.
Summary
- Define typed inputs using a struct with
#[derive(Props)], including optional fields marked with#[prop(optional)]. - Implement the component as an async function with
#[component], accepting&Cxand props, returning aViewbuilt with theview!macro. - Consume components inside other
view!blocks using tag-like syntax; the macro handles context propagation, props building, and trait implementation automatically.
Frequently Asked Questions
What is the Cx parameter in component functions?
Cx (Context) is a request-scoped handle provided by the Topcoat runtime that gives components access to handlers, state, and other request-specific resources. It must be the first parameter in any #[component] function and is automatically cloned when passed to child components or async handlers.
Can I use components outside of view! blocks?
While components are designed for declarative use within view! macros, you can instantiate them programmatically by calling the generated Component::render method directly with a Cx reference and manually constructed props. However, this bypasses the ergonomic props builder and is generally only needed for testing or dynamic composition.
How do optional props affect the component builder?
When a field is marked with #[prop(optional)], the generated builder allows the field to be omitted. If omitted, the field receives its Default value (typically None for Option<T> types). This enables flexible APIs where callbacks or configuration values can be provided conditionally without requiring placeholder values at every call site.
Where can I find real-world examples of custom components?
The repository includes production-ready examples in examples/ui/src/components/button.rs, demonstrating patterns for event handling, conditional styling, and children propagation. The macro's full documentation resides in crates/topcoat-view/macro/docs/component.md, covering additional attributes and edge cases.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →