Skip to content

Commit b65f25b

Browse files
committed
docs: drop void and async from the InfiniteList recipe
`void` was there to quiet a floating-promise lint rule, and `async` promised a result nobody awaits. `loadMore` now starts the request and returns nothing, so the observer calls `loadMore()` and the button takes `onClick={loadMore}` directly. The two-argument `.then` is deliberate. `.catch` after it would also swallow a bug thrown by the state updates in the success path and report it to the user as a failed request. Same slop in the lazy-loading section I added two commits ago, caught while looking: LazyTile reported visibility to its parent through a useEffect on `inView`, which is the effect the recipe rewrite just removed. It uses `onChange` now. That fires exactly once under `triggerOnce`, so the parent counts with a number instead of an id array with an includes() dedupe guard, and the useCallback around it goes too. Verified against the browser's own observer, since the docs preview pane was hidden and a hidden page delivers no intersections at all: the recipe still passes all seven cases, and the tiles start deferred with no img in the DOM, count to exactly six on scroll, and do not double-count when scrolled away and back.
1 parent a114253 commit b65f25b

3 files changed

Lines changed: 25 additions & 29 deletions

File tree

‎apps/docs/docs/guides/recipes.mdx‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,25 +154,25 @@ export function InfiniteList<T>({
154154
const [hasNextPage, setHasNextPage] = useState(true);
155155
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
156156

157-
async function loadMore() {
157+
function loadMore() {
158158
if (status === "loading" || !hasNextPage) return;
159159

160160
setStatus("loading");
161-
try {
162-
const page = await loadPage(items.length);
163-
setItems((current) => [...current, ...page.items]);
164-
setHasNextPage(page.hasNextPage);
165-
setStatus("idle");
166-
} catch {
167-
setStatus("error");
168-
}
161+
loadPage(items.length).then(
162+
(page) => {
163+
setItems((current) => [...current, ...page.items]);
164+
setHasNextPage(page.hasNextPage);
165+
setStatus("idle");
166+
},
167+
() => setStatus("error"),
168+
);
169169
}
170170

171171
const { ref } = useInView({
172172
rootMargin: "400px 0px",
173173
skip: status !== "idle" || !hasNextPage,
174174
onChange: (inView) => {
175-
if (inView) void loadMore();
175+
if (inView) loadMore();
176176
},
177177
});
178178

@@ -192,7 +192,7 @@ export function InfiniteList<T>({
192192
{hasNextPage ? (
193193
<>
194194
<div ref={ref} aria-hidden="true" />
195-
<button disabled={status === "loading"} onClick={() => void loadMore()}>
195+
<button disabled={status === "loading"} onClick={loadMore}>
196196
{status === "error" ? "Try again" : "Load more"}
197197
</button>
198198
</>
@@ -208,6 +208,8 @@ There is no effect here, and that is the point. An empty list puts the sentinel
208208

209209
One `status` beats separate `loading` and `error` booleans: the component cannot be loading and failed at the same time, and the retry button reads its own label from it.
210210

211+
`loadMore` is not `async`. Nothing awaits it, so returning a promise would only invite a caller to try. It starts the request and reports the outcome through state, which is what both the observer and the button want.
212+
211213
`skip` does two jobs. It keeps a second request from starting while one is in flight, and because the hook drops and recreates the observer when `skip` flips back, it re-arms the sentinel. A page too short to push the sentinel out of view keeps loading until the viewport fills.
212214

213215
`rootMargin` starts the request before the sentinel reaches the viewport. Tune the `400px` value to your network and item size. This example sends `items.length` as the next page's `skip`; use whatever pagination your API expects.

‎apps/docs/pages/_home/Landing.tsx‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,7 @@ function ApiStepCard({
544544
/* Lazy strip: defer the work until the target is near */
545545
/* ================================================================== */
546546

547-
/* Stand-in artwork. Inline SVG keeps the demo self-contained, so a tile that
548-
has not been reached yet costs nothing and the page pulls no extra files. */
547+
/* Stand-in artwork, inline so an unreached tile costs nothing. */
549548
function art(from: string, to: string, seed: number) {
550549
const svg =
551550
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 120">` +
@@ -570,10 +569,7 @@ const PHOTOS = [
570569

571570
function LazyStrip() {
572571
const ref = useSectionSignal("lazy", "Lazy loading");
573-
const [loaded, setLoaded] = useState<string[]>([]);
574-
const markLoaded = useCallback((id: string) => {
575-
setLoaded((current) => (current.includes(id) ? current : [...current, id]));
576-
}, []);
572+
const [loaded, setLoaded] = useState(0);
577573

578574
return (
579575
<section className="rio-section rio-lazy" ref={ref}>
@@ -593,7 +589,7 @@ function LazyStrip() {
593589
<LazyTile
594590
index={i}
595591
key={photo.id}
596-
onLoad={markLoaded}
592+
onLoad={() => setLoaded((n) => n + 1)}
597593
photo={photo}
598594
/>
599595
))}
@@ -602,7 +598,7 @@ function LazyStrip() {
602598
<aside className="rio-lazy__panel">
603599
<div className="rio-counter">
604600
<span className="rio-counter__num">
605-
{loaded.length}
601+
{loaded}
606602
<span className="rio-counter__of">/{PHOTOS.length}</span>
607603
</span>
608604
<span className="rio-counter__label">images requested</span>
@@ -651,17 +647,16 @@ function LazyTile({
651647
}: {
652648
photo: { id: string; src: string };
653649
index: number;
654-
onLoad: (id: string) => void;
650+
onLoad: () => void;
655651
}) {
656652
const { ref, inView } = useInView({
657653
rootMargin: "200px 0px",
658654
triggerOnce: true,
655+
onChange: (visible) => {
656+
if (visible) onLoad();
657+
},
659658
});
660659

661-
useEffect(() => {
662-
if (inView) onLoad(photo.id);
663-
}, [inView, onLoad, photo.id]);
664-
665660
return (
666661
<figure className="rio-shot" data-loaded={inView} ref={ref}>
667662
<div className="rio-shot__frame">

‎apps/docs/pages/_home/home.css‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,8 +1014,8 @@
10141014
border-color: color-mix(in srgb, var(--rio-emerald) 45%, transparent);
10151015
}
10161016

1017-
/* The frame reserves the tile's space before the image exists, so loading one
1018-
never shifts the rows below it. This is the part that is easy to skip. */
1017+
/* Reserves the tile's space before the image exists, so loading one never
1018+
shifts the rows below it. */
10191019
.rio-shot__frame {
10201020
aspect-ratio: 4 / 3;
10211021
border-radius: 8px;
@@ -1099,9 +1099,8 @@
10991099
color: var(--rio-emerald);
11001100
}
11011101

1102-
/* The badge sits over the frame, so it has to read against both the empty
1103-
placeholder (theme-coloured) and the loaded artwork (always dark enough
1104-
for white). Two states, not one colour that only works on one of them. */
1102+
/* The badge sits over the frame, so it needs one color for the theme-tinted
1103+
placeholder and another for the artwork it covers once loaded. */
11051104
.rio-shot__index {
11061105
position: absolute;
11071106
top: 1rem;

0 commit comments

Comments
 (0)