Source: https://monarchui.dev/components/load-more/

Components · Tables and data grid

# LoadMore

Cursor pagination footer with loading, error and retry, end of list and announcements.

@rookcom/monarch-react

Component

## With useLoadMore

## Usage examples

### States

### With react-query

## Purpose

Appends the next page of a cursor-paginated list on request. `LoadMore` is the footer; `useLoadMore` holds the pages, cursor and status, while the product supplies the fetch.

### Use when

- Feeds, audit logs, search results and message history served with a cursor (`?cursor=…&limit=…`).
- The reader scans forward and rarely needs to jump to page 7.

### Avoid when

- Readers need to jump to a page or know where they are in a fixed set: use Pagination.
- The whole set is small enough to render at once.

## Design principles

- The button is always there while there is more, even with `autoLoad`: scrolling is a convenience, not the only way.
- Every outcome is announced politely: how many items arrived, a failure, or the end of the list.
- Errors are recoverable in place: the same button becomes Retry and repeats the failed cursor.
- Data fetching stays in the product. The hook takes any `fetchPage`, and the component works with any data library through its props.

## API reference

```tsx
import { LoadMore, useLoadMore } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `LoadMore` | component | client | `.mn-load-more` |
| `useLoadMore` | hook | client | – |

`useLoadMore({ fetchPage, initialPage?, loadOnMount? })` returns `items`, `status` (`idle`, `loading`, `error`, `done`), `error`, `hasMore`, `total`, `loadMore`, `retry`, `reset` and `loadMoreProps` to spread onto `<LoadMore>`.

`fetchPage(cursor, { signal })` receives an AbortSignal that fires on `reset` and unmount; responses that arrive after a reset are ignored.

With react-query or SWR, skip the hook and pass `hasMore`, `loading`, `error`, `onLoadMore` and `loadedCount` from `useInfiniteQuery` directly.

The ref is forwarded to the root `<div>`.

Generated from the TypeScript declarations of `packages/react/src/components/data-grid/load-more`.

## Properties

### `LoadMore`

The footer of a cursor-paginated list: a Load more button with loading, error and retry, end-of-list states and polite announcements, plus optional auto-load on scroll. Pair it with `useLoadMore`, or drive it from any data library through its props.

| Prop | Type | Description |
| --- | --- | --- |
| `hasMore` required | `boolean` | There is another page to load. |
| `onLoadMore` required | `() => void` | Loads the next page. |
| `autoLoad` | `boolean` | Load the next page when the control scrolls into view (IntersectionObserver). The button always stays as the fallback and for keyboard users; auto-load pauses after an error. Default `false`. |
| `autoLoadMargin` | `string` | IntersectionObserver root margin for `autoLoad`. Default `200px`. |
| `className` | `string` |  |
| `endLabel` | `ReactNode` | Shown when everything is loaded; `null` hides it. Default "That’s everything". |
| `error` | `boolean` | The last fetch failed. Shows `errorLabel` and turns the button into Retry. Auto-load pauses. |
| `errorLabel` | `ReactNode` | Error message. Default "Couldn’t load more." |
| `id` | `string` |  |
| `label` | `ReactNode` | Button text. Default "Load more". |
| `loadedCount` | `number` | Items currently on screen. Changes are announced ("20 more items loaded"). |
| `loading` | `boolean` | A page is being fetched. The button stays focusable and shows a spinner. |
| `loadingLabel` | `ReactNode` | Button text while loading. Default "Loading…". |
| `noun` | `{ one: string; other: string; }` | Noun for counts and announcements. Default `{ one: 'item', other: 'items' }`. |
| `onRetry` | `(() => void)` | Called by the Retry button. Default: `onLoadMore`. |
| `retryLabel` | `ReactNode` | Retry button text. Default "Retry". |
| `showCount` | `boolean` | Show "Showing 40 of 120 items" next to the button. Default `true` when `loadedCount` is given. |
| `totalCount` | `number` | Total items, when the server knows it. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `autoLoad` | `'false'``'true'` | Also loads when the footer scrolls into view (IntersectionObserver, `autoLoadMargin` ahead). |
| `showCount` | `'true'``'false'` | “Showing 40 of 120 items” beside the button. On by default when `loadedCount` is given. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Idle | `data-state="idle"` | Load more button, optional count. |
| Loading | `data-state="loading"` | The button shows a spinner and “Loading…”, stays focusable and ignores repeat clicks (`aria-busy`). |
| Error | `data-state="error"` | `errorLabel` in danger text; the button reads Retry; auto-load pauses until it succeeds. |
| End of list | `data-state="done"` | The button is replaced by `endLabel` (“That’s everything”), or nothing when `endLabel={null}`. |

## Interaction behaviour

- Activating the button calls `onLoadMore`, or `onRetry` (default `onLoadMore`) after an error.
- The hook ignores `loadMore` while a request is in flight and after the last page.
- `reset()` clears the items and loads the first page again, aborting any request in flight.
- When the last page arrives while the button has focus, focus moves to the end message, so it is not lost to the page body.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves to the Load more (or Retry) button. |
| Enter / Space | Loads the next page, or retries. |

## Accessibility

Role

button inside a group with a polite status region

Accessible name

“Load more”, “Loading…” or “Retry” (customisable labels).

- A visually hidden `role="status"` region announces “20 more items loaded.”, the error and the end of the list.
- Loading uses `aria-busy` and `aria-disabled` instead of `disabled`, so keyboard focus stays on the button.
- Auto-load never removes the button, so keyboard and switch users can always load explicitly.

## Responsive behaviour

- The count, message and button wrap onto separate lines on narrow screens and stay centred.

## Theme support

- Muted text for the count and end message, danger text for the error; the button is a secondary Button. All are Monarch tokens.

## Do and don't

### Do

- Give a `noun` so counts and announcements read naturally (“3 more entries loaded”).
- Pass `totalCount` when the server knows it.
- Call `reset()` when filters or search change.

### Don't

- Do not hide the button when using `autoLoad`.
- Do not use LoadMore where readers must reach a specific page; use Pagination.
- Do not fetch inside the component; pass `fetchPage` or the query state.

## Related components

- [Pagination](https://monarchui.dev/components/pagination/): Numbered pages with jumps.
- [DataTable](https://monarchui.dev/components/data-table/): Tables whose rows are loaded in pages.
- [Button](https://monarchui.dev/components/button/): The control LoadMore renders.
- [LoadingState](https://monarchui.dev/components/loading-state/): The first load of an empty list.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/) |

## Testing coverage

- Loads the first page on mount, more on click, then shows the end of the list and announces each step.
- Works with Enter and Space and moves focus to the end message when the button disappears.
- Shows and announces an error; Retry repeats the same cursor, including a failed first page.
- `reset` reloads from the first page and ignores a stale response.
- Ignores repeat `loadMore` calls while loading.
- Starts from `initialPage` without a request.
- Auto-loads on intersection and pauses after an error, keeping the button.
- Works controlled by props without the hook.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/load-more |

Tests: `packages/react/src/components/data-grid/load-more`

## All example code

### With useLoadMore

```tsx
import { useState } from 'react';
import { LoadMore, useLoadMore, type LoadMorePage } from '@rookcom/monarch-react';

type Entry = { id: number; title: string };

const ALL: Entry[] = Array.from({ length: 9 }, (_, i) => ({
  id: i + 1,
  title: `Audit entry ${i + 1}`,
}));

/** A fake cursor endpoint: pages of 3, a short delay, and one failure on the third page. */
function demoFetch(): (cursor: number | undefined) => Promise<LoadMorePage<Entry, number>> {
  let failed = false;
  return (cursor) =>
    new Promise((resolve, reject) => {
      setTimeout(() => {
        const start = cursor ?? 0;
        if (start === 6 && !failed) {
          failed = true;
          reject(new Error('Network error'));
          return;
        }
        const items = ALL.slice(start, start + 3);
        resolve({
          items,
          nextCursor: start + 3 < ALL.length ? start + 3 : null,
          total: ALL.length,
        });
      }, 600);
    });
}

function CursorListDemo() {
  const [fetchPage] = useState(demoFetch);
  const list = useLoadMore<Entry, number>({
    fetchPage,
    initialPage: { items: ALL.slice(0, 3), nextCursor: 3, total: ALL.length },
  });
  return (
    <div style={{ width: '100%' }}>
      <ul aria-label="Audit log" style={{ margin: 0, paddingInlineStart: '1.25rem' }}>
        {list.items.map((e) => (
          <li key={e.id}>{e.title}</li>
        ))}
      </ul>
      <LoadMore {...list.loadMoreProps} noun={{ one: 'entry', other: 'entries' }} />
    </div>
  );
}

<CursorListDemo />
```

### States

```tsx
import { LoadMore } from '@rookcom/monarch-react';

<LoadMore hasMore loadedCount={50} totalCount={214} onLoadMore={() => {}} />
<LoadMore hasMore loading loadedCount={50} onLoadMore={() => {}} />
<LoadMore hasMore error loadedCount={50} onLoadMore={() => {}} />
<LoadMore hasMore={false} loadedCount={214} totalCount={214} onLoadMore={() => {}} />
```

### With react-query

```tsx
import { LoadMore } from '@rookcom/monarch-react';

<LoadMore hasMore loadedCount={20} onLoadMore={() => {}} autoLoad={false} />
```
