Hilla and React Views

This section documents the current Vaadin release line — Vaadin 24 LTS / 25.x, Java 17+, Spring Boot 3 / Jakarta EE 10 — as published at the official Vaadin documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Flow (server-side Java) is the authoring style used throughout, with Hilla / React shown where it differs; Vaadin 7 and the pre-Flow architecture appear only as migration contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vaadin ships major releases roughly twice a year and its ecosystem iterates.

This section’s bibliography lists the reference material consulted while preparing these pages.

Hilla is the second programming model in Vaadin: the browser UI is written in React and TypeScript, and it calls type-safe endpoints generated from your Java services. The backend, build, theming and security are the same as Flow — one project, two ways to write views. This page follows the Hilla documentation and Add a React View. Hilla was previously called Fusion; the name changed in 2022, the model did not.

A single Spring Boot backend serves both Flow views written in Java and Hilla views written in React that call generated type-safe endpoints; both render the same Vaadin Web Components
Figure 1. Flow and Hilla on one backend

Endpoints

Annotate a Spring service with @BrowserCallable (an alias of @Endpoint) and it becomes callable from the browser. Vaadin generates a TypeScript module with one typed function per public method:

@BrowserCallable
@AnonymousAllowed
public class PersonEndpoint {

    private final PersonRepository repository;

    public PersonEndpoint(PersonRepository repository) {
        this.repository = repository;
    }

    public List<Person> list() {
        return repository.findAll();
    }

    public Person save(@Valid Person person) {
        return repository.save(person);
    }
}
import { PersonEndpoint } from 'Frontend/generated/endpoints';

const people: Person[] = await PersonEndpoint.list();
await PersonEndpoint.save(edited);   // Person is a generated TS type

Bean Validation annotations on the parameters are enforced on the server and surfaced to the client. See Endpoints.

Views and routing

A .tsx file under src/main/frontend/views/ is a route; the path mirrors the folder structure (views/people/list.tsx/people/list). An exported config object sets the title and menu placement; @vaadin/react-router handles navigation.

// src/main/frontend/views/people.tsx
import { ViewConfig } from '@vaadin/hilla-file-router/types.js';
import { Grid } from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { useEffect, useState } from 'react';
import { PersonEndpoint } from 'Frontend/generated/endpoints';
import type Person from 'Frontend/generated/com/example/Person';

export const config: ViewConfig = { title: 'People', menu: { order: 1 } };

export default function PeopleView() {
  const [people, setPeople] = useState<Person[]>([]);
  useEffect(() => { PersonEndpoint.list().then(setPeople); }, []);
  return (
    <Grid items={people}>
      <GridColumn path="firstName" />
      <GridColumn path="lastName" />
    </Grid>
  );
}

See Routing and Routing.

Forms

useForm binds a generated model to fields, runs the same validators as the server, and exposes submit:

import { useForm } from '@vaadin/hilla-react-form';
import PersonModel from 'Frontend/generated/com/example/PersonModel';
import { TextField } from '@vaadin/react-components/TextField.js';
import { Button } from '@vaadin/react-components/Button.js';

function PersonForm() {
  const { model, field, submit, invalid } = useForm(PersonModel, {
    onSubmit: async (person) => { await PersonEndpoint.save(person); },
  });
  return (
    <>
      <TextField label="First name" {...field(model.firstName)} />
      <TextField label="Last name" {...field(model.lastName)} />
      <Button theme="primary" disabled={invalid} onClick={submit}>Save</Button>
    </>
  );
}

Security

Every endpoint is denied by default. Open it with a class- or method-level annotation, exactly as on the Flow side (Security):

@BrowserCallable
@RolesAllowed("ADMIN")
public class AdminEndpoint {
    @AnonymousAllowed
    public String publicPing() { return "ok"; }
}

On the client, useAuth() exposes the authenticated user, roles and login / logout, and route access is checked from each view’s config.rolesAllowed. See Security.

Reactive endpoints and signals

A method returning Flux<T> streams values to the client as an async iterable — for live prices, logs or progress:

public Flux<Integer> counter() {
    return Flux.interval(Duration.ofSeconds(1)).map(Long::intValue);
}
for await (const n of CounterEndpoint.counter()) { setCount(n); }

Full-stack signals go further: a NumberSignal / ValueSignal shared between the server and every connected client stays in sync automatically, giving collaborative state with no manual wiring. See Reactive Endpoints and Full-Stack Signals.

Mixing Flow and Hilla, and choosing

One application can serve Flow routes and Hilla routes side by side — a Flow admin area and a Hilla customer portal, for instance — sharing the security config, the theme and the domain layer.

Choose When

Flow

The team is Java-first; the UI is forms-and-tables heavy; you want data binding and Grid lazy loading with no REST layer to design.

Hilla

The team knows React; the UI is highly interactive or custom; you want the browser-side ecosystem (Styling and UI Libraries) with a type-safe Java backend instead of hand-written REST.

See also