Triggering Multiple Routes from a Single Form - FastHTML & HTMX

March 23, 2025
fasthtml

When building FastHTML apps that need to display multiple components on a page, the traditional (at least the first way I tried!) approach is to have a single route handler that computes and returns all components:

@app.get("/dashboard")
def dashboard():
    # Potentially slow computations
    profile_data = compute_profile_data()  # Fast: 100ms
    activity_data = compute_activity_data()  # Medium: 500ms
    analytics_data = compute_analytics_data()  # Slow: 2000ms

    return Titled("Dashboard",
        profile_component(profile_data),
        activity_component(activity_data),
        analytics_component(analytics_data)
    )

The bottleneck issue: The page load time is the sum of each of the components because they are running in series. For example, if there is one really slow component, the entire page load is blocked by that component. But what if the user can get each component as soon as it's ready?

Solution: Parallel HTMX Requests

Instead of a single route that returns everything, we can use HTMX to trigger multiple requests in parallel and update different parts of the page as responses arrive.

Key HTMX attributes:

  • hx-trigger: Specifies what event triggers the request and from where
  • hx-include: Specifies which form inputs to include in the request

Implementation Example

import time

from fasthtml.common import *

app, _ = fast_app()


@app.get("/")
def home():
    return (
        Title("Multi-Route Form Example"),
        # Form with fields
        Form(
            id="main-form",
        )(
            Fieldset(
                Label("Your Name", Input(type="text", name="username", id="username")),
                Label("Message", Textarea(name="message", id="message")),
            ),
        ),
        # Button outside the form
        Button("Update All", id="trigger-button"),
        # Container for components
        Div(
            # Component 1 - Fast
            Div(
                hx_get="/greeting",
                hx_trigger="click from:#trigger-button",
                hx_include="#username",
                hx_swap="innerHTML",
                id="greeting-component",
            )(
                H3("Personal Greeting"),
                P("..."),
            ),
            # Component 2 - Medium
            Div(
                hx_post="/echo",
                hx_trigger="click from:#trigger-button",
                hx_include="#username, #message",
                hx_swap="innerHTML",
                id="echo-component",
            )(
                H3("Message Echo"),
                P("..."),
            ),
            # Component 3 - Slow
            Div(
                hx_get="/time",
                hx_trigger="click from:#trigger-button",
                hx_swap="innerHTML",
                id="time-component",
            )(
                H3("Current Time (with delay)"),
                P("..."),
            ),
        ),
    )


# Fast endpoint (~100ms)
@app.get("/greeting")
def greeting(username: str = None):
    return Div(H3("Personal Greeting"), P(f"Hello, {username or 'Anonymous'}!"))


# Medium endpoint (~500ms)
@app.post("/echo")
def echo(username: str = None, message: str = None):
    time.sleep(0.5)  # Simulate processing time
    return Div(
        H3("Message Echo"),
        P(f"From: {username or 'Anonymous'}"),
        P("Message: " + (message or "No message provided")),
    )


# Slow endpoint (~2000ms)
@app.get("/time")
def current_time():
    time.sleep(2)  # Simulate slow processing
    current_time = time.strftime("%H:%M:%S")
    return Div(
        H3("Current Time (with delay)"),
        P(f"Server time: {current_time}"),
        P("This component took 2 seconds to load"),
    )


serve()

You can paste the above code and run it directly if you have FastHTML installed in your environment. Check out the network tab to see multiple requests firing.

How It Works

  1. The page loads with placeholder "..." content in each component
  2. When the user inputs info and clicks the button, three separate requests fire in parallel (check out the network tab)
  3. As each request completes, its corresponding component updates independently
  4. The fast component appears almost immediately, while slower ones take their time
  5. The UI progressively populates as responses arrive, rather than waiting for the slowest

Implementation Details

1. Form Setup

  • Create a form with fields but don't give it an action or method
  • Fields need IDs so we can reference them with hx-include
  • Button has an ID so components can listen for clicks on it

2. Component Configuration

Each component div needs:

  • hx-trigger="click from:#trigger-button" - Listen for button clicks
  • hx-include="#fieldId" - Include specific form fields in the request
  • hx-get or hx-post - Specify the endpoint
  • hx-swap="innerHTML" - Replace the component's content with the response

3. Backend Routes

Each route handles only what it needs:

  • /greeting - Only needs username, returns quickly
  • /echo - Needs username and message, medium processing time
  • /time - Doesn't need any form data, slow processing time

Advantages

  1. Perceived Performance: Users see the fastest components first
  2. Parallel Processing: Server can process requests concurrently
  3. Isolated Failures: If one component fails, others still work

Notes

  • The example simulates different processing times with time.sleep()
  • Real-world applications might have naturally varying processing times (database queries, API calls, complex calculations)
  • Add hx-indicator if you want loading indicators for each component
  • Can be combined with automatic polling by adding hx-trigger="every:5s" to components that need regular updates