# Lists that load more

> A page's list over a big table holds the first answer's rows (about 2 MB of them). rows.HasMore says whether the set holds rows past what the page has, and rows.LoadMore() reads the next answer and appends it. DataGrid pages by itself, LoadMoreButton(rows) puts the same offer under a list you draw with foreach, and a list that is every row it will ever have offers nothing.

<!-- id: ui-load-more · area: ui · stability: stable · html: https://osysharp.com/reference/ui/load-more/ -->

## Summary        {#summary}

A page's query is sent in answers of about 2 MB of rows ([How many rows a page is sent](https://osysharp.com/reference/ui/read-limits/)), so a list over a table bigger than that
holds the **first answer** — and looks complete. Two members of the list say otherwise: **`rows.HasMore`** is true while
rows exist past what the page holds, and **`rows.LoadMore()`** reads the next answer, from the row after the last one
held, and appends it. The list grows in place; every slot that reads it re-renders.

```osy title="the offer under a list, there only while there is more" test app=ui-load-more
entity Reading {
  [Required, MaxLength(40)] string Station;
  DateTime TakenAt;
  decimal Value;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Route("/readings")] [Render(CSR)] [AllowAnonymous]
component ReadingsPage() {
  live var readings = Reading.OrderByDescending(r => r.TakenAt).ToList();

  render {
    Stack(gap: 1, p: 6) {
      foreach (var r in readings) { Text($"{r.Station}: {r.Value}"); }
      if (readings.HasMore) { Button("Load more", onPress: () => readings.LoadMore()); }
    }
  }
}
```

## Signature      {#signature}

```osy syntax
rows.HasMore                    // bool — the set holds rows past what this list holds
rows.LoadMore();                // read the next answer and append it (awaited in a body)
LoadMoreButton(rows);           // the kit's offer: drawn only while rows.HasMore
DataGrid("…", rows: rows, …);   // pages by itself — nothing to add
```

## Description    {#description}

### The kit's controls page by themselves   {#kit}

`DataGrid` draws **Load more** under its rows whenever the rows it was handed hold more, so a table over a page's query
pages with nothing written. A list you draw yourself takes `LoadMoreButton(rows)` under it — the same offer, gone once
the last answer is in:

```osy title="a table that pages, and a hand-drawn list with the kit's button" test app=ui-load-more
[Route("/stations")] [Render(CSR)] [AllowAnonymous]
component StationsPage() {
  live var readings = Reading.OrderBy(r => r.Station).ToList();

  render {
    Stack(gap: 4, p: 6) {
      DataGrid("Readings", rows: readings,
               columns: [ new GridColumn<Reading> { Name = "Station", Label = "Station", Value = r => r.Station } ]);
      Stack(gap: 1) {
        foreach (var r in readings) { Text(r.Station); }
        LoadMoreButton(readings, label: "More readings");
      }
    }
  }
}
```

⚠ A `DataGrid`'s search, column filters and sort work on the rows it has **loaded**. To search the whole table, put the
search in the page's query (`Reading.Where(r => r.Station.Contains(find))`), which the database answers over every row.

### Asked of the list, wherever it travels   {#value}

`HasMore` is a question about the **value**, not about how it was declared — which is why a control handed a page's
query as a plain `T[] rows` can ask it. A list that is not a window of anything answers `false`, and `LoadMore()` on it
does nothing: an array built in memory, a server function's answer, a list computed from a query
(`readings.Where(r => r.Value > 0)` — ask `readings`, not the list made from it), or a `.Take(n)` the page has filled.
That is the true answer for each: it is every row it will ever have.

### In an action, it is awaited   {#awaited}

`LoadMore()` in an action body waits for the next answer, so the statement after it sees the rows it appended:

```osy title="the count after loading is the count of what loaded" test app=ui-load-more
[Route("/readings/count")] [Render(CSR)] [AllowAnonymous]
component ReadingCount() {
  live var readings = Reading.OrderBy(r => r.TakenAt).ToList();
  int held = 0;

  action More() {
    readings.LoadMore();
    held = readings.Count;
  }

  render {
    Stack(gap: 2, p: 6) {
      Text($"{held} loaded");
      Button("Load the next readings", onPress: More);
    }
  }
}
```

### Where can it not be written?   {#refused}

**In a render slot**, `LoadMore()` is refused: a slot re-runs whenever what it reads changes, so a read written there
would run on every render. Call it from a press. **In code that runs on the server**, both are refused: a function's
list is every row its query names — server code is never windowed — so there is nothing more to ask for. Bound a server
read with `.Take(n)` and `.Skip(n)`.

### How big is the next answer?   {#size}

As large as the answer budget allows — about 2 MB of rows, measured as they are sent — so a page that `Select`s the few
fields it shows ([Select the fields a page shows](https://osysharp.com/reference/ui/narrow-page-reads/)) gets more rows a press. The next answer starts at the row after the last one
held, so paging never skips or repeats a row. When the set changes while a page holds it (a `live` list re-reads after
a commit), the re-read keeps every row already loaded.

## See also        {#see-also}

- [How many rows a page is sent](https://osysharp.com/reference/ui/read-limits/) — how many rows a page is sent, and what is never capped
- [A list or table with no bound](https://osysharp.com/reference/ui/unbounded-lists/) — what `osy lint` says about a list that does not page
- [A picker over every row — the SearchPicker](https://osysharp.com/reference/ui/search-pickers/) — choosing one row of many: a search the database answers
- [Select the fields a page shows](https://osysharp.com/reference/ui/narrow-page-reads/) — more rows an answer by reading fewer fields
