> Markdown version of [Paginated Data](https://vaadin.com/docs/latest/building-apps/forms-data/add-grid/paginated-data). Section index: [llms.txt](https://vaadin.com/docs/latest/building-apps/llms.txt)

# Add a Paginated Grid

Paginated data refers to datasets that are too large to fit in memory, even after filtering. Instead, **the data is loaded in chunks (pages)** from an application service as needed. **Both filtering and sorting are delegated to the service layer** (typically a database).

In Vaadin, the Grid component has built-in support for paginated data (often referred to as lazy loading in API documentation). When the user scrolls or changes the sorting/filtering criteria, the grid requests the appropriate data page from the backend service. To the user, this appears as a seamless scrolling experience.

This guide starts with a complete example that you can copy-paste into your project if you want. It then breaks down the example into sections that explain how to populate, filter, and sort the grid. The guide does not cover setting up the Grid component itself; for that, see the [Grid](https://vaadin.com/docs/latest/components/grid.md) component documentation.

## <a id="copy-paste-into-your-project"></a>Copy-Paste into Your Project

If you want to quickly try out a paginated grid in your Vaadin application, copy-paste the following code into your project:

`PaginatedGridView.java`

```java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.provider.QuerySortOrder;
import com.vaadin.flow.data.provider.SortDirection;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;

@Route("building-apps/grid/paginated-grid")
public class PaginatedGridView extends VerticalLayout {

    PaginatedGridView() {
        // In a real application, this would be a Spring bean
        // injected through the constructor
        var service = new ItemService();

        // Create components
        var filterField = new TextField();
        filterField.setPlaceholder("Filter items");
        // Update the filter value as soon as the user stops typing
        filterField.setValueChangeMode(ValueChangeMode.LAZY);

        var grid = new Grid<Item>();
        grid.addColumn(Item::id).setHeader("ID").setSortProperty("id");
        grid.addColumn(Item::name).setHeader("Name").setSortProperty("name");
        // Fetch a slice from the service whenever the grid needs more data.
        // Include sorting and filtering info.
        grid.setItems(query -> service
                .findItems(
                        query.getOffset(),
                        query.getLimit(),
                        toItemSortOrders(query.getSortOrders()),
                        filterField.getValue())
                .stream());

        // Refresh the grid whenever the text field changes
        filterField.addValueChangeListener(
                e -> grid.getDataProvider().refreshAll());

        // Layout components
        add(filterField);
        add(grid);
        setSizeFull();
    }

    // Convert Vaadin's sort orders into the sort orders of the service
    private static List<ItemSortOrder> toItemSortOrders(
            List<QuerySortOrder> sortOrders) {
        return sortOrders.stream()
                .map(sortOrder -> new ItemSortOrder(sortOrder.getSorted(),
                        sortOrder.getDirection() == SortDirection.ASCENDING))
                .toList();
    }

    // In a real application, this would be in its own file
    static class ItemService {
        private final List<Item> items = new ArrayList<>();

        ItemService() {
            for (int i = 0; i < 1000; ++i) {
                items.add(new Item(i, "Item " + i));
            }
        }

        public List<Item> findItems(int offset, int limit,
                List<ItemSortOrder> sortOrders, String filterString) {
            // In a real application, this would query a database
            var filterLower = filterString.toLowerCase().trim();
            var stream = items.stream();
            if (!filterLower.isEmpty()) {
                stream = stream.filter(item -> item
                    .name()
                    .toLowerCase()
                    .contains(filterLower));
            }
            return stream
                    .sorted(toComparator(sortOrders))
                    .skip(offset)
                    .limit(limit)
                    .toList();
        }

        // In a real application, the sorting would take place in the database,
        // NOT in memory as shown here
        private final Comparator<Item> defaultComparator = Comparator
                .comparing(Item::id);

        private Comparator<Item> toComparator(List<ItemSortOrder> sortOrders) {
            return sortOrders.stream().map(this::toComparator)
                    .reduce(Comparator::thenComparing)
                    .orElse(defaultComparator);
        }

        private Comparator<Item> toComparator(ItemSortOrder sortOrder) {
            if (sortOrder.property().equals("name")) {
                return observeSortDirection(Comparator.comparing(Item::name),
                        sortOrder.ascending());
            } else {
                return observeSortDirection(defaultComparator,
                        sortOrder.ascending());
            }
        }

        private Comparator<Item> observeSortDirection(
                Comparator<Item> ascendingComparator, boolean ascending) {
            return ascending ? ascendingComparator
                    : ascendingComparator.reversed();
        }
    }

    record Item(long id, String name) {
    }

    record ItemSortOrder(String property, boolean ascending) {
    }
}
```

For more detailed walk-through of the example code, continue reading below.

## <a id="getting-the-data"></a>Getting the Data

The Grid component can request pages either by using a page number and size, or by using an offset and limit. If the dataset is sorted, you have to sort it before applying pagination.

This example uses offset-based pagination. The application service defines its own `ItemSortOrder` record for the sorting information, so that its API doesn’t depend on Vaadin’s data provider classes:

`PaginatedGridView.java`

```java
// In a real application, this would be in its own file
static class ItemService {

    public List<Item> findItems(int offset, int limit,
            List<ItemSortOrder> sortOrders, String filterString) {
        // In a real application, this would query a database
    }
}

record Item(long id, String name) {
}

record ItemSortOrder(String property, boolean ascending) {
}
```

In a real world application, the service would be in its own file and the data would be queried from a database.

## <a id="populating-the-grid"></a>Populating the Grid

You populate the grid with paginated data by passing it a callback function that fetches data pages on demand. The callback function receives a `Query` object that contains information about the requested page, sorting, and filtering criteria.

`PaginatedGridView.java`

```java
// Fetch a slice from the service whenever the grid needs more data.
// Include sorting and filtering info.
grid.setItems(query -> service
        .findItems(
                query.getOffset(),
                query.getLimit(),
                toItemSortOrders(query.getSortOrders()),
                filterField.getValue())
        .stream());
```

### <a id="spring-data-support"></a>Spring Data Support

If you are using Spring Data, use the `setItemsPageable()` method. It passes a Spring Data `Pageable` object to the callback function, so you don’t have to convert the page and sorting information yourself. The application service can pass it on to a [Spring Data repository](https://vaadin.com/docs/latest/building-apps/forms-data/persistence/spring-data.md):

```java
// Spring Data repository:
public interface ItemRepository extends JpaRepository<Item, Long> {
    List<Item> findByNameContainingIgnoreCase(String name, Pageable pageable);
}

// Application service:
@Service
@Transactional(readOnly = true)
@PreAuthorize("isAuthenticated()")
public class ItemService {

    private final ItemRepository repository;

    ItemService(ItemRepository repository) {
        this.repository = repository;
    }

    public List<Item> findItems(String filterString, Pageable pageable) {
        return repository.findByNameContainingIgnoreCase(filterString, pageable);
    }
}

// In the view class:
grid.setItemsPageable(pageable -> service
    .findItems(filterField.getValue(), pageable)
);
```

> **Note: Can the View Call the Repository Directly?**
>
> Yes, for a simple, read-only grid, if the access control of the view is enough and no business logic is involved. Otherwise, call an application service, as in the example above. See [Calling Repositories from Views](https://vaadin.com/docs/latest/building-apps/business-logic/add-service.md#calling-repositories-from-views) for details.

## <a id="filtering"></a>Filtering

The filtering happens in the application service and the filter string is passed to the service via the callback function. Because of this, you have to refresh the grid whenever the filter string changes:

`PaginatedGridView.java`

```java
// Fetch a slice from the service whenever the grid needs more data.
// Include sorting and filtering info.
grid.setItems(query -> service
        .findItems(
                query.getOffset(),
                query.getLimit(),
                toItemSortOrders(query.getSortOrders()),
                filterField.getValue())
        .stream());

// Refresh the grid whenever the text field changes
filterField.addValueChangeListener(
        e -> grid.getDataProvider().refreshAll());
```

## <a id="sorting"></a>Sorting

Sorting happens in the application service and the sort orders are passed to the service via the callback function. Only columns with a specific **sort property** are sortable:

`PaginatedGridView.java`

```java
grid.addColumn(Item::id).setHeader("ID").setSortProperty("id");
grid.addColumn(Item::name).setHeader("Name").setSortProperty("name");
```

Sort properties are strings that are passed to the application service. They typically correspond to database column names, but in this example they are mapped to the record fields in the `Item` class.

The grid describes the requested sort orders as Vaadin `QuerySortOrder` objects. The view converts them into `ItemSortOrder` records before calling the service:

`PaginatedGridView.java`

```java
// Convert Vaadin's sort orders into the sort orders of the service
private static List<ItemSortOrder> toItemSortOrders(
        List<QuerySortOrder> sortOrders) {
    return sortOrders.stream()
            .map(sortOrder -> new ItemSortOrder(sortOrder.getSorted(),
                    sortOrder.getDirection() == SortDirection.ASCENDING))
            .toList();
}
```
