Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/pagination-solid2-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ Migrate to Solid.js v2.0 (beta.14)

- `isServer` now imported from `@solidjs/web` (not `solid-js/web`)
- `createPagination`: page clamping when pages count decreases is now implemented via a derived memo instead of `createComputed` (which was removed in Solid 2.0). The clamping is reactive and automatic.
- `createInfiniteScroll`: removed `createResource` dependency (removed in Solid 2.0). Fetching is now implemented with `createEffect` and a cancellation pattern. The `pages.loading` and `pages.error` resource properties are no longer available; use `end()` or wrap the fetcher to handle errors externally.
- `createInfiniteScroll`: redesigned around per-page async reads instead of a flat accumulated array (resolves #797). `pages()` now returns the `{ content, fetching, error, retry }` bundle for every requested page, in order — feed it directly to `<For>`. `content` is a genuine async value, so it also works inside `<Loading>`/`<Errored>` boundaries for consumers who prefer that over reading `fetching`/`error` directly. Internally, pages are cached and disposed via `mapArray` (the same primitive `<For>` is built on), so shrinking `pageCount` now correctly disposes the pages that fall out of range — previously they leaked. `page`/`setPage`/`setPages`/`getPage` are removed in favor of `pageCount`/`setPageCount` and reading `pages()` directly; `end` no longer folds in errors (a failed page pauses auto-loading until retried via `retry()`, rather than permanently ending the scroll); added `reset()` to dispose every page and start over.
- `batch()` calls removed — Solid 2.0 batches updates automatically via microtasks. Tests require `flush()` after signal writes to observe committed values.
- All internal signals use `{ ownedWrite: true }` to allow setters to be called from reactive scopes and event handlers without triggering ownership warnings.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@solid-primitives/a11y": "workspace:*",
"@solidjs/signals": "2.0.0-beta.14",
"@solidjs/web": "2.0.0-beta.15",
"@storybook/addon-docs": "^10.4.6",
"@storybook/addon-docs": "10.4.4",
"@testing-library/jest-dom": "^6.9.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.20.0",
Expand All @@ -64,7 +64,7 @@
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"solid-js": "2.0.0-beta.15",
"storybook": "^10.4.6",
"storybook": "10.4.4",
"storybook-solidjs-vite": "^10.5.2",
"typescript": "^6.0.3",
"vite": "^6.4.3",
Expand Down
65 changes: 55 additions & 10 deletions packages/pagination/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<p>
<img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=pagination" alt="Solid Primitives pagination">
<img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=Pagination" alt="Solid Primitives pagination">
</p>

# @solid-primitives/pagination

[![docs](https://img.shields.io/badge/-docs%20%26%20demos-blue?style=for-the-badge)](https://primitives.solidjs.community/package/pagination)
[![size](https://img.shields.io/badge/size-1.68_kB-blue?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/pagination)
[![version](https://img.shields.io/npm/v/@solid-primitives/pagination?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/pagination)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-0.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
[![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-3.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
[![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)

A primitive that creates all the reactive data to manage your pagination:
Expand Down Expand Up @@ -142,6 +142,8 @@ return <For each={segment()}>{item => <Item item={item} />}</For>;

Combines [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) with a page-based fetcher to provide an easy way to implement infinite scrolling. Browser-only: the loader and fetching are skipped on the server.

Each page is its own independent async unit, so it can be rendered with `<Loading>`/`<Errored>` for idiomatic suspense and retry, or read via plain `fetching`/`error` signals if you'd rather not use boundaries.

### How to use it

```tsx
Expand All @@ -150,32 +152,75 @@ const [pages, setEl, { end }] = createInfiniteScroll(fetcher);

return (
<div>
<For each={pages()}>{item => <h4>{item}</h4>}</For>
<For each={pages()}>
{page => (
// Note: use `page.retry`, not Errored's own `reset` — `content` only
// re-fetches when `retry()` bumps its internal version signal, so a
// bare `reset()` would just re-surface the same cached rejection.
// Errored also won't hand control back to <Loading> while that retry
// is in flight, so the fallback watches `fetching()` itself.
<Errored
fallback={err => (
<Show when={!page.fetching()} fallback={<h4>Retrying…</h4>}>
<button onClick={page.retry}>Retry: {String(err())}</button>
</Show>
)}
>
<Loading fallback={<h4>Loading…</h4>}>
<For each={page.content()}>{item => <h4>{item}</h4>}</For>
</Loading>
</Errored>
)}
</For>
<Show when={!end()}>
<h1 ref={setEl}>Loading...</h1>
</Show>
</div>
);
```

Prefer plain signals over boundaries? Skip `<Loading>`/`<Errored>` and use `fetching()`/`error()`/`retry()` directly:

```tsx
<For each={pages()}>
{page => (
<Show
when={!page.error()}
fallback={<button onClick={page.retry}>Retry: {String(page.error())}</button>}
>
<Show when={!page.fetching()} fallback={<h4>Loading…</h4>}>
<For each={page.content()}>{item => <h4>{item}</h4>}</For>
</Show>
</Show>
)}
</For>
```

### Definition

```ts
type InfiniteScrollPage<T> = {
content: Accessor<T[]>;
fetching: Accessor<boolean>;
error: Accessor<unknown>;
retry: () => void;
};

function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>): [
pages: Accessor<T[]>,
pages: Accessor<InfiniteScrollPage<T>[]>,
loader: (el: Element) => void,
options: {
page: Accessor<number>;
setPage: Setter<number>;
setPages: Setter<T[]>;
pageCount: Accessor<number>;
setPageCount: Setter<number>;
end: Accessor<boolean>;
fetching: Accessor<boolean>;
error: Accessor<unknown>;
reset: () => void;
},
];
```

`end` is `true` when the fetcher returns an empty array or when an error occurs.
- `pages()` is the `{ content, fetching, error, retry }` bundle for every page requested so far, in order — feed it directly to `<For>`. `retry()` re-runs that page's fetcher and clears its error.
- `end` is `true` once a page fetch returns zero items. A failed page does **not** set `end` — the sentinel just pauses auto-loading until that page is retried.
- `reset()` disposes every page's reactive state and starts over from the first page. It does **not** abort an in-flight fetch — if one resolves or rejects after disposal, its result is ignored by the same stale-request check `retry()` relies on.

## Changelog

Expand Down
Loading
Loading