Components · Tables and data grid
LoadMore
Cursor pagination footer with loading, error and retry, end of list and announcements.
With useLoadMore
- Audit entry 1
- Audit entry 2
- Audit entry 3
Showing 3 of 9 entries
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 />Usage examples
States
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
import { LoadMore } from '@rookcom/monarch-react';
<LoadMore hasMore loadedCount={20} onLoadMore={() => {}} autoLoad={false} />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
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, oronRetry(defaultonLoadMore) after an error. - The hook ignores
loadMorewhile 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-busyandaria-disabledinstead ofdisabled, 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
nounso counts and announcements read naturally (“3 more entries loaded”). - Pass
totalCountwhen 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
fetchPageor the query state.
Related components
- Pagination: Numbered pages with jumps.
- DataTable: Tables whose rows are loaded in pages.
- Button: The control LoadMore renders.
- LoadingState: The first load of an empty list.
Dependencies
| Kind | Depends on |
|---|---|
| npm packages | react |
| Monarch 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.
resetreloads from the first page and ignores a stale response.- Ignores repeat
loadMorecalls while loading. - Starts from
initialPagewithout 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