Displaying data with Vaadin Grid: lazy loading, filtering, and selection
Show large datasets with Vaadin Grid: define columns, render components in cells, lazy load from a Spring Data repository, add filters, and handle selection.
The workhorse of business apps
Lists of orders, customers, invoices, and log entries are at the heart of most business applications. Vaadin's Grid component displays tabular data with sorting, resizable columns, selection, and smooth scrolling through hundreds of thousands of rows, all controlled from Java.
The examples target Vaadin 24 or later with Spring Boot and Spring Data JPA.
We will display this entity:
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
private String customer;
private LocalDate orderDate;
private BigDecimal total;
@Enumerated(EnumType.STRING)
private OrderStatus status;
// getters and setters omitted
}
Defining columns
Create the grid without automatic columns, then add exactly the columns you want:
var grid = new Grid<>(Order.class, false);
grid.addColumn(Order::getId).setHeader("#").setAutoWidth(true).setFlexGrow(0);
grid.addColumn(Order::getCustomer).setHeader("Customer").setSortProperty("customer");
grid.addColumn(new LocalDateRenderer<>(Order::getOrderDate, "dd MMM yyyy"))
.setHeader("Date").setSortProperty("orderDate");
grid.addColumn(order -> currency.format(order.getTotal()))
.setHeader("Total").setTextAlign(ColumnTextAlign.END).setSortProperty("total");
grid.addColumn(Order::getStatus).setHeader("Status");
grid.addThemeVariants(GridVariant.LUMO_ROW_STRIPES);
grid.setSizeFull();
Passing false as the second constructor argument disables automatic columns generated from bean properties. Explicit columns give you control over order, headers, and formatting.
setAutoWidth(true)sizes a column to its content.setFlexGrow(0)stops a column from taking extra space.setSortProperty("...")makes the column sortable by that entity property, which matters for lazy loading below.
Components inside cells
Use addComponentColumn to render buttons, badges, or links:
grid.addComponentColumn(order -> {
var badge = new Span(order.getStatus().label());
badge.getElement().getThemeList().add(switch (order.getStatus()) {
case PAID -> "badge success";
case CANCELLED -> "badge error";
default -> "badge";
});
return badge;
}).setHeader("Status");
grid.addComponentColumn(order -> new Button(VaadinIcon.EDIT.create(), e -> openEditor(order)))
.setHeader("Actions").setAutoWidth(true).setFlexGrow(0);
Component columns are convenient but create a server-side component for each visible row. For very large grids or many such columns, a LitRenderer is more efficient because it renders a lightweight template in the browser.
In-memory or lazy data?
For small datasets, simply pass a collection:
grid.setItems(orderService.findRecent());
All items are kept in server memory and the grid handles sorting itself.
For large tables this does not scale. Loading 200,000 orders into memory for every user would exhaust the server. Instead, let the grid ask for just the rows it needs as the user scrolls.
Lazy loading with Spring Data
Add a pageable query to your repository:
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
Page<Order> findByCustomerContainingIgnoreCase(String customer, Pageable pageable);
}
Then give the grid a fetch callback:
grid.setItems(query -> orderRepository
.findAll(VaadinSpringDataHelpers.toSpringPageRequest(query))
.stream());
VaadinSpringDataHelpers.toSpringPageRequest converts the grid's offset, limit, and current sort order into a Spring Data PageRequest. When the user scrolls, the grid calls the lambda again for the next chunk. When the user clicks a sortable column header, the query arrives with the corresponding setSortProperty values.
With only a fetch callback, the grid does not know the total number of rows; the scrollbar grows as data loads. If you want an exact scrollbar, provide a count callback too:
grid.setItems(
query -> orderRepository.findAll(VaadinSpringDataHelpers.toSpringPageRequest(query)).stream(),
query -> (int) orderRepository.count());
Counting can be expensive on huge tables, so only add it when the exact size is worth it.
Filtering
Add a search field above the grid and use its value inside the callback:
var filter = new TextField();
filter.setPlaceholder("Search customer...");
filter.setPrefixComponent(VaadinIcon.SEARCH.create());
filter.setClearButtonVisible(true);
filter.setValueChangeMode(ValueChangeMode.LAZY);
filter.addValueChangeListener(e -> grid.getDataProvider().refreshAll());
grid.setItems(query -> orderRepository
.findByCustomerContainingIgnoreCase(filter.getValue(), VaadinSpringDataHelpers.toSpringPageRequest(query))
.stream());
ValueChangeMode.LAZY waits until the user pauses typing before sending the value, which avoids a database query on every keystroke. Calling refreshAll() makes the grid discard cached rows and fetch again with the new filter.
For several filters (status, date range, customer), build a Specification and use findAll(specification, pageable) from JpaSpecificationExecutor.
Selection
By default, the grid supports selecting a single row. React to it to show details or open an editor:
grid.asSingleSelect().addValueChangeListener(event -> {
Order selected = event.getValue();
if (selected != null) {
orderDetails.show(selected);
}
});
For bulk actions, switch to multi-selection, which adds checkboxes:
grid.setSelectionMode(Grid.SelectionMode.MULTI);
var cancelSelected = new Button("Cancel selected", e -> {
Set<Order> selected = grid.getSelectedItems();
orderService.cancel(selected);
grid.getDataProvider().refreshAll();
});
Implement equals and hashCode on your entity (typically based on the ID). The grid relies on them to recognize selected items when it reloads data.
Item details
Show extra information below a row without leaving the grid:
grid.setItemDetailsRenderer(new ComponentRenderer<>(order -> {
var details = new VerticalLayout();
details.add(new Span("Shipping address: " + order.getShippingAddress()));
details.add(new Span("Notes: " + Objects.requireNonNullElse(order.getNotes(), "-")));
return details;
}));
Clicking a row toggles its details panel.
Performance tips
- Use lazy loading for anything that can grow beyond a few thousand rows.
- Only make columns sortable when there is a matching database index or the table is small.
- Prefer
LitRendererover component columns for large grids. - Fetch only the data you display. A DTO projection is often better than a full entity with lazy associations.
- Avoid the N+1 problem: if a column reads
order.getCustomer().getName()from a lazy association, fetch the association together with the page.
Exercise
Create an orders view with a lazily loaded grid, a text filter for the customer name, a ComboBox<OrderStatus> filter, and a DatePicker range. Combine all filters into a Spring Data Specification, keep the selected status in the URL query string, and add a "Mark as shipped" bulk action for selected rows.
