Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/infinite-scroll-initial-page-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solid-primitives/pagination": minor
---

add `initialPageCount` option to `createInfiniteScroll`, letting SSR request pages up front (for SEO/perceived speed) instead of always starting empty on the server
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 deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
]
},
"catalog": {
"solid-js": "2.0.0-beta.15",
"@solidjs/web": "2.0.0-beta.15",
"solid-js": "2.0.0-beta.20",
"@solidjs/web": "2.0.0-beta.20",
"tsdown": "^0.22.3",
"unplugin-solid": "^1.0.0"
},
Expand Down
532 changes: 261 additions & 271 deletions deno.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/interaction/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/interaction",
"version": "1.0.0-next.0",
"version": "1.0.0-next.1",
"description": "SolidJS primitive for detecting interactions outside a given element.",
"license": "MIT",
"exports": "./src/index.ts",
Expand Down
73 changes: 61 additions & 12 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.81_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 @@ -140,7 +140,9 @@ return <For each={segment()}>{item => <Item item={item} />}</For>;

## `createInfiniteScroll`

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.
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. The sentinel-based auto-loading is browser-only, but fetching itself isn't: pass `initialPageCount` to request pages during SSR too, so the first page(s) are present in the server-rendered HTML for SEO and perceived speed.

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

Expand All @@ -150,32 +152,79 @@ 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
function createInfiniteScroll<T>(fetcher: (page: number) => Promise<T[]>): [
pages: Accessor<T[]>,
type InfiniteScrollPage<T> = {
content: Accessor<T[]>;
fetching: Accessor<boolean>;
error: Accessor<unknown>;
retry: () => void;
};

function createInfiniteScroll<T>(
fetcher: (page: number) => Promise<T[]>,
options?: { initialPageCount?: number },
): [
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.
- `options.initialPageCount` sets how many pages are requested up front — same fetch/retry/error mechanics as any other page. Defaults to `0` on the server and `1` in the browser; raise it on the server to render initial content into the SSR'd HTML.
- `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 `initialPageCount` pages. 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
2 changes: 1 addition & 1 deletion packages/pagination/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@solid-primitives/pagination",
"version": "1.0.0-next.1",
"version": "1.0.0-next.2",
"description": "A primitive that creates all the reactive data to manage your pagination.",
"license": "MIT",
"exports": "./dist/index.js",
Expand Down
Loading
Loading