LiveComponents

Build reactive, real-time interfaces without writing JavaScript. Server-side components with automatic state synchronization.

Basics

Create a component
@LiveComponentImpl
public class Counter extends LiveComponent {

    @State
    private int count = 0;

    @Action
    public void increment() { count++; }

    @Action
    public void decrement() { count--; }

    public int getCount() { return count; }

    @Override
    public String template() {
        return "components/counter.html";
    }
}
Component template
<div live:id="{{ _id }}">
    <h2>Count: {{ count }}</h2>

    <button live:click="increment">+ Increment</button>
    <button live:click="decrement">- Decrement</button>

    <div live:loading>Updating...</div>
</div>
Use in a template
{{ component('Counter') | raw }}

{{ component('UserCard', { userId: user.id }) | raw }}
Setup

No setup required. The LiveComponents runtime script is injected automatically before </body> and SessionMiddleware is applied globally to all routes.

Props & Lifecycle

@Prop — mount-time props

Fields annotated with @Prop are injected before onMount() and never exposed in client state.

Component
@LiveComponentImpl
public class UserCard extends LiveComponent {

    @Prop
    private int userId;

    @State
    private User user;

    @Override
    public void onMount() {
        user = User.findById(userId);
    }

    @Override
    public String template() {
        return "components/user-card.html";
    }
}
Template
{{ component('UserCard', { userId: user.id }) | raw }}
Lifecycle hooks
@Override
public void onMount() {
    // Called once after props injection, before first render
    // Use to load initial data
}

@Override
public void onUpdate() {
    // Called after every action execution, before re-render
    // Use to recompute derived state
}
Request helpers
session("key")              // Read from session
session("key", value)       // Write to session
param("name")               // Query/form parameter
param("name", "default")    // With fallback
routeParam("id")            // Route parameter (:id)

Actions

@Action — explicit action contract

Only methods annotated with @Action can be invoked from the client. Any unlisted method throws a ComponentException immediately.

@Action
public void save() {
    // Called via live:click="save"
}

@Action
public void delete(int id) {
    // Called via live:click="delete({{ id }})"
}
ComponentResponse — redirect & events

Actions can return a ComponentResponse to trigger a client-side navigation or dispatch a custom event after re-render.

@Action
public ComponentResponse save() {
    // Persist data...
    return redirect("/dashboard");
}

@Action
public ComponentResponse delete() {
    return emit("toast:show", Map.of("message", "Deleted!"));
}

// Listen to the event on the client
window.addEventListener("toast:show", (e) => {
    console.log(e.detail.message); // "Deleted!"
});

Examples

Real-time search with live:model
Component
@LiveComponentImpl
public class SearchFilter extends LiveComponent {

    @State
    private String search = "";

    private final List<String> items = List.of(
        "React", "Vue", "Angular", "Svelte", "Spring Boot"
    );

    public List<String> getFilteredItems() {
        if (search == null || search.isEmpty()) return items;
        return items.stream()
            .filter(i -> i.toLowerCase().contains(search.toLowerCase()))
            .collect(Collectors.toList());
    }

    public String getSearch() { return search; }

    @Override
    public String template() { return "components/search.html"; }
}
Template
<div live:id="{{ _id }}">
    <input
        live:model="search"
        live:debounce="300"
        type="text"
        placeholder="Search..."
        value="{{ search }}"
    >
    <div>{{ filteredItems.size() }} results</div>
    {% for item in filteredItems %}
        <div>{{ item }}</div>
    {% endfor %}
</div>
Auto-refresh with live:poll
Component
@LiveComponentImpl
public class Dashboard extends LiveComponent {

    @State
    private int activeUsers = 0;

    @Override
    public void onMount() {
        activeUsers = userService.getActiveCount();
    }

    @Action
    public void refreshStats() {
        activeUsers = userService.getActiveCount();
    }

    public int getActiveUsers() { return activeUsers; }

    @Override
    public String template() { return "components/dashboard.html"; }
}
Template
<div
    live:id="{{ _id }}"
    live:poll.5s="refreshStats"
>
    <h2>Active Users: {{ activeUsers }}</h2>
    <div live:loading.class="opacity-100" class="opacity-0">Updating...</div>
</div>
Lazy mount

Mount a component after first paint to avoid blocking page render. The innerHTML is shown as a skeleton during fetch.

<!-- Plain -->
<div live:lazy="PlayerSearch"></div>

<!-- With props -->
<div live:lazy="UserCard" live:props='{"userId": 42}'></div>

<!-- With skeleton -->
<div live:lazy="PlayerSearch">
    <div class="animate-pulse h-8 bg-zinc-800 rounded"></div>
</div>

Directives reference

Directive Description
live:click="action"Calls an @Action on click
live:click="action({{ id }})"Calls an action with a parameter
live:model="field"Two-way binding — syncs input value to state on input
live:debounce="300"Debounces live:model updates (ms)
live:blurUpdate model on blur instead of input
live:enterUpdate model on Enter key
live:submit="action"Intercepts form submission and calls action
live:poll="5s"Auto-refresh every N seconds/minutes
live:poll.5s="action"Auto-refresh and call a specific action
live:init="action"Calls action on component mount
live:confirm="message"Shows a confirmation dialog before calling action
live:loadingShows element while action is in progress
live:loading.class="cls"Adds class during loading
live:loading.add="cls"Adds class during loading, removes after
live:lazy="Component"Lazy mounts a component after first paint
live:props='{"key": val}'Props passed to a lazy-mounted component
Breaking changes in v1.1.0
  • • Remove {{ livecomponents_scripts | raw }} from all templates — script is now injected automatically before </body>
  • • Remove @Before(SessionMiddleware.class) from controller methods — middleware is now global
Breaking changes in v1.5.0
  • @Action is now required on all client-callable methods — add it to prev(), next(), and any action method or they will throw ComponentException at runtime
  • live:lazy on inputs (update on Enter key) has been renamed to live:enter