How to Implement Server-Side Rendering with the #[shard] Attribute in Topcoat

Topcoat's #[shard] attribute enables automatic server-side rendering of page fragments by re-executing async functions whenever reactive signal dependencies change, delivering updated HTML without full-page reloads.

The tokio-rs/topcoat framework provides a unique approach to partial server-side rendering through the #[shard] macro. This attribute transforms regular async functions into reactive server-side fragments that automatically synchronize with client-side state. When you implement server-side rendering with the #[shard] attribute, you create fine-grained updates that execute database queries or API calls on the server while maintaining reactive client-side signals.

Understanding the #[shard] Architecture

The shard system bridges client-side reactivity with server-side execution through a signal-driven request cycle.

The Signal-Driven Execution Model

Topcoat's architecture relies on signals to trigger server-side work. Inside your components, you create signals that hold client-side state—such as text input values or selection states. When these signals change, the framework automatically serializes the new values, sends them to the server, and re-executes the corresponding #[shard] function. The returned HTML fragment replaces the previous DOM content without refreshing the entire page.

Request Context and Async Boundaries

Every shard function receives a &Cx (request context) as its first argument, as implemented in crates/topcoat-runtime/grammar/src/shard.rs. This context provides access to the router, asset bundles, and other per-request services. Because shards execute on the server, you can perform asynchronous work—such as database lookups or external HTTP requests—before constructing the view response.

Setting Up a Shard Function

A valid shard function must follow specific signature requirements enforced by the Topcoat compiler. The function must be declared as async and return a Result type. The first parameter must always be cx: &Cx, followed by the arguments you want to track for changes.

// crates/topcoat-runtime/macro/docs/shard.md defines these constraints
#[shard]
async fn combobox_content(cx: &Cx, input: String) -> Result {
    // Perform async server-side work
    let results = search_fruit(cx, &input).await;
    
    view! {
        <div>
            <b>"results:"</b>
            for item in results {
                <div>(item)</div>
            }
        </div>
    }
}

The shard only re-executes when the specific arguments passed from the client component change. Topcoat automatically handles the serialization, network request, and DOM diffing when these tracked arguments update.

Triggering Server-Side Re-renders from the Client

To connect client-side interactivity with server-side rendering, you invoke the shard function inside a component's view! macro, passing signal values as arguments. The framework monitors these dependencies and manages the server communication automatically.

In examples/shard/src/main.rs, the combobox component demonstrates this pattern:

#[component]
async fn combobox() -> Result {
    view! {
        // Reactive state declaration
        signal input = String::new();

        <div>
            // Update signal on user input
            <input :value=$(input.get())
                   @input=$(|e: Event| input.set(e.target.value))>

            // Server shard automatically re-renders when input changes
            { combobox_content(input: $(input.get())) }
        </div>
    }
}

When the user types into the input field, the signal updates, triggering a server request that executes combobox_content with the new input value. The server returns fresh HTML, which Topcoat swaps into the DOM.

Complete Working Example

The full implementation in examples/shard/src/main.rs demonstrates the integration between pages, components, signals, and shards:

use topcoat::{
    Result,
    asset::{AssetBundle, RouterBuilderAssetExt},
    context::Cx,
    router::{Router, RouterBuilderDiscoverExt, page},
    runtime::{Event, shard},
    view::{component, view},
};

#[tokio::main]
async fn main() {
    topcoat::start(
        Router::builder()
            .assets(AssetBundle::load().unwrap())
            .discover()
            .build(),
    )
    .await
    .unwrap();
}

#[page("/")]
async fn home() -> Result {
    view! {
        <!DOCTYPE html>
        <html>
            <head>
                { topcoat::dev::script() }
                { topcoat::runtime::script() }
            </head>
            <body>
                { combobox() }
            </body>
        </html>
    }
}

#[component]
async fn combobox() -> Result {
    view! {
        signal input = String::new();
        <div>
            <input :value=$(input.get())
                   @input=$(|e: Event| input.set(e.target.value))>
            { combobox_content(input: $(input.get())) }
        </div>
    }
}

#[shard]
async fn combobox_content(cx: &Cx, input: String) -> Result {
    let results = search_fruit(cx, &input).await;
    view! {
        <div>
            <b>"results:"</b>
            for item in results {
                <div>(item)</div>
            }
        </div>
    }
}

async fn search_fruit(_cx: &Cx, input: &str) -> Vec<&'static str> {
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    let needle = input.to_lowercase();
    const FRUIT: [&str; 35] = [
        "apple", "apricot", "banana", "blackberry", "blueberry", "cherry",
        "coconut", "cranberry", "date", "dragonfruit", "elderberry", "fig",
        "grape", "grapefruit", "guava", "honeydew", "kiwi", "lemon",
        "lime", "lychee", "mango", "nectarine", "orange", "papaya",
        "passionfruit", "peach", "pear", "persimmon", "pineapple",
        "plum", "pomegranate", "raspberry", "strawberry", "tangerine",
        "watermelon",
    ];
    FRUIT.into_iter()
        .filter(|fruit| fruit.contains(&needle))
        .collect()
}

Key Implementation Details

When implementing server-side rendering with the #[shard] attribute, remember these critical constraints defined in the Topcoat source:

  • Async requirement: Shard functions must be declared async and return Result.
  • Context parameter: The first argument must be cx: &Cx to receive the request context.
  • Tracked arguments: Only arguments explicitly passed in the view call trigger re-execution; untracked data does not invalidate the cache.
  • Response type: The function must return a view! { ... } block or any type implementing IntoResponse.
  • Macro location: The shard parsing logic resides in crates/topcoat-runtime/grammar/src/shard.rs, with documentation available in crates/topcoat-runtime/macro/docs/shard.md.

Summary

  • Server-side fragments: The #[shard] attribute marks functions that execute on the server and return HTML fragments when reactive dependencies change.
  • Signal integration: Client-side signals drive re-execution; pass signal values directly to shard invocations in your view! macros.
  • Request context: Access router and assets through the mandatory &Cx first parameter in every shard function.
  • Async capabilities: Perform database queries, API calls, or other asynchronous work before rendering the response view.
  • Automatic synchronization: Topcoat handles serialization, network requests, and DOM updates without manual intervention.

Frequently Asked Questions

What are the exact signature requirements for a #[shard] function?

A #[shard] function must be declared as async and return Result. The first parameter must be cx: &Cx (the request context), followed by any arguments you want to track for changes. According to crates/topcoat-runtime/grammar/src/shard.rs, the macro validates these constraints at compile time to ensure proper server-side execution context.

How does Topcoat know when to re-execute a shard?

Topcoat tracks only the arguments explicitly passed to the shard invocation in your view! macro. When any signal value passed as an argument changes, the framework serializes the new values, sends a request to the server, and re-executes the shard function. Arguments not included in the view call are not monitored for changes.

Can I perform database queries inside a shard function?

Yes. Because #[shard] functions execute on the server and receive a &Cx context parameter, you can perform any asynchronous work including database queries, external API calls, or file system operations. The shard completes this work before returning the view! block that renders the final HTML fragment.

Where is the #[shard] macro defined in the source code?

The core parsing logic for the #[shard] attribute resides in crates/topcoat-runtime/grammar/src/shard.rs. Additional documentation and usage constraints are specified in crates/topcoat-runtime/macro/docs/shard.md. The runtime integration that handles the client-server communication is part of the broader topcoat-runtime crate.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →