From vaadin-development
Guides building client-side React/Hilla views in Vaadin 25 with file-based routing, @BrowserCallable endpoints, ViewConfig, reactive endpoints, signals, and type-safe backend calls.
npx claudepluginhub marcushellberg/vaadin-development-plugin --plugin vaadin-developmentThis skill uses the workspace's default tool permissions.
Use the Vaadin MCP tools (`search_vaadin_docs`) to look up the latest documentation whenever uncertain about a specific API detail. Always set `vaadin_version` to `"25"` and `ui_language` to `"react"`.
Implements use cases by creating Vaadin views, forms, grids for UI layer and jOOQ queries for data access in Java web apps.
Builds React 19 components and Next.js 15 apps with responsive layouts, client-side state management using Zustand, and server components. Optimizes performance, accessibility, and data fetching.
Renders React entirely client-side for interactive SPAs like dashboards, admin tools, and prototypes where SEO is unnecessary. Covers Vite setup, React Router, and performance mitigations.
Share bugs, ideas, or general feedback.
Use the Vaadin MCP tools (search_vaadin_docs) to look up the latest documentation whenever uncertain about a specific API detail. Always set vaadin_version to "25" and ui_language to "react".
Client-side (React/Hilla) views run in the browser and communicate with Java endpoints over HTTP. Choose them when:
Use server-side Flow views when you need rapid prototyping with pure Java, direct database access from UI code, or full server-side security for every interaction.
A single Vaadin 25 application can mix both models — Flow views and Hilla views coexist via the generated routes.tsx.
src/main/frontend/
views/ # File-based routing root
@layout.tsx # Main application layout
@index.tsx # Root view (/)
about.tsx # /about
products/
@index.tsx # /products
{productId}.tsx # /products/:productId
components/ # Shared React components (not routes)
generated/ # Auto-generated endpoint clients (do not edit)
endpoints.ts
src/main/java/
com/example/services/
ProductService.java # @BrowserCallable endpoint
The generated/ directory is rebuilt automatically during development. Import generated endpoint clients from Frontend/generated/endpoints.
Views are .tsx files in src/main/frontend/views/. The file path determines the URL.
| File | URL | Purpose |
|---|---|---|
@index.tsx | Directory index (/) | Landing page for a directory |
@layout.tsx | — | Wrapping layout with <Outlet/> |
about.tsx | /about | Static route |
{productId}.tsx | /products/:productId | Required parameter |
{{search}}.tsx | /search/:search? | Optional parameter |
{...wildcard}.tsx | /* | Wildcard (catch-all) |
_utils.tsx | — | Ignored by router (underscore prefix) |
Access parameters with React Router's useParams:
import { useParams } from 'react-router';
export default function ProductView() {
const { productId } = useParams();
// fetch product by productId...
}
import { useNavigate } from 'react-router';
function SaveButton() {
const navigate = useNavigate();
const handleSave = async () => {
await ProductService.save(product);
navigate('/products');
};
return <Button onClick={handleSave}>Save</Button>;
}
Export a config object from any view to customize its route metadata:
import type { ViewConfig } from '@vaadin/hilla-file-router/types.js';
export default function DashboardView() {
return <div>Dashboard content</div>;
}
export const config: ViewConfig = {
title: 'Dashboard',
loginRequired: true,
rolesAllowed: ['ADMIN', 'MANAGER'],
menu: {
title: 'Dashboard',
icon: 'vaadin:dashboard',
order: 1,
},
};
Key properties: title, route, loginRequired, rolesAllowed, skipLayouts, menu (title, icon, order, exclude), detail. See the reference file for the full property table.
Use createMenuItems() from @vaadin/hilla-file-router/runtime.js to populate navigation:
import { createMenuItems } from '@vaadin/hilla-file-router/runtime.js';
import { SideNavItem } from '@vaadin/react-components/SideNavItem.js';
import { Icon } from '@vaadin/react-components/Icon.js';
{createMenuItems().map(({ to, icon, title }) => (
<SideNavItem path={to} key={to}>
{icon && <Icon icon={icon} slot="prefix" />}
{title}
</SideNavItem>
))}
Create @layout.tsx in any directory. It wraps sibling and child views via <Outlet/>. Use AppLayout with SideNav for the standard application shell:
import { AppLayout } from '@vaadin/react-components/AppLayout.js';
import { DrawerToggle } from '@vaadin/react-components/DrawerToggle.js';
import { Suspense } from 'react';
import { Outlet } from 'react-router';
export default function MainLayout() {
return (
<AppLayout primarySection="drawer">
<div slot="drawer" className="flex flex-col justify-between h-full p-m">
<h1 className="text-l m-0">My App</h1>
{/* SideNav with createMenuItems() here */}
</div>
<DrawerToggle slot="navbar" aria-label="Menu toggle" />
<Suspense fallback={<div>Loading...</div>}>
<Outlet />
</Suspense>
</AppLayout>
);
}
Set skipLayouts: true in ViewConfig to render a view without parent layouts (e.g., login page).
Annotate a Java service with @BrowserCallable to expose it to the frontend. Hilla generates type-safe TypeScript clients automatically.
@BrowserCallable
@AnonymousAllowed
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
@RolesAllowed("ADMIN")
public Product save(@Valid @Nonnull Product product) {
return repo.save(product);
}
public @Nonnull List<@Nonnull Product> findAll() {
return repo.findAll();
}
}
import { ProductService } from 'Frontend/generated/endpoints';
import { useEffect } from 'react';
import { useSignal } from '@vaadin/hilla-react-signals';
import type Product from 'Frontend/generated/com/example/Product';
export default function ProductListView() {
const products = useSignal<Product[]>([]);
useEffect(() => {
ProductService.findAll().then(data => {
products.value = data;
});
}, []);
return (
<ul>
{products.value.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Every @BrowserCallable method is denied by default. Add one of:
| Annotation | Access |
|---|---|
@AnonymousAllowed | Anyone, no login required |
@PermitAll | Any authenticated user |
@RolesAllowed("ROLE") | Users with the specified role(s) |
@DenyAll | Explicitly blocked (the default) |
Place on the class for a default, or on individual methods to override.
Catch EndpointValidationError (Bean Validation failures with per-field validationErrorData), EndpointError (server exceptions with message and type), or plain errors (network failures). All from @vaadin/hilla-frontend. See the reference file for a complete error handling template.
Return a Flux from a @BrowserCallable method to push data to the browser:
@BrowserCallable
public class TimeService {
@AnonymousAllowed
public Flux<@Nonnull String> getClock() {
return Flux.interval(Duration.ofSeconds(1))
.onBackpressureDrop()
.map(i -> Instant.now().toString());
}
@AnonymousAllowed
public EndpointSubscription<@Nonnull String> getClockCancellable() {
return EndpointSubscription.of(getClock(), () -> {
// cleanup when client unsubscribes
});
}
}
import { TimeService } from 'Frontend/generated/endpoints';
import type { Subscription } from '@vaadin/hilla-frontend';
const sub = useSignal<Subscription<string> | undefined>(undefined);
// Start
sub.value = TimeService.getClockCancellable()
.onNext(time => { serverTime.value = time; })
.onError(err => console.error(err));
// Stop
sub.value?.cancel();
sub.value = undefined;
The Subscription object provides cancel(), onNext(), onError(), onComplete(), and onSubscriptionLost() callbacks. See the reference file for the full API table.
Vaadin provides useSignal as a lightweight alternative to React's useState. Signals skip unnecessary re-renders by updating only the DOM nodes that read the signal value.
import { useSignal, useComputed } from '@vaadin/hilla-react-signals';
export default function CounterView() {
const count = useSignal(0);
const doubled = useComputed(() => count.value * 2);
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled: {doubled.value}</p>
<Button onClick={() => count.value++}>Increment</Button>
</div>
);
}
.valueuseComputed creates derived signals that update automaticallyChoose the right model — use client-side views for high-traffic pages, offline needs, or complex interactivity. Use Flow for admin panels, internal tools, or rapid prototyping with pure Java.
Keep endpoints stateless — @BrowserCallable services should not store per-user state in fields. Use method parameters and return values. Inject Spring services for data access.
Always add security annotations — endpoints are denied by default. Every public method needs @AnonymousAllowed, @PermitAll, or @RolesAllowed. Combine with loginRequired and rolesAllowed in ViewConfig for defense in depth.
Use signals over useState — useSignal from @vaadin/hilla-react-signals provides fine-grained reactivity with less boilerplate. Reserve useState for cases where you need React's rendering lifecycle (e.g., transitions).
Follow file naming conventions — use @index.tsx for directory indices, @layout.tsx for layouts, {param}.tsx for parameters. Prefix non-route files with _.
Handle endpoint errors — always wrap endpoint calls in try/catch. Distinguish EndpointValidationError (field-level issues) from EndpointError (server exceptions) and network errors.
Use ViewConfig for access control — set loginRequired and rolesAllowed on views, and matching security annotations on endpoints. This gives both client-side route protection and server-side enforcement.