Lots of UI is a ring: a carousel that loops, a theme toggle that rotates light → dark → system → light, a "next tip" button that never runs out. useCycleList captures that pattern — hold an index into an array and move it forward or backward with wraparound, so stepping off the end lands back at the start and stepping before the start lands on the last item.
Implement useCycleList(items, startIndex = 0). It returns [activeItem, controls], where controls exposes the current index plus next(), prev(), and setIndex(i) — all of which keep the index inside the array's bounds by wrapping.
function useCycleList(items, startIndex = 0) {
// returns [activeItem, { index, next, prev, setIndex }]
}
const [color, { next }] = useCycleList(['red', 'green', 'blue']);
// color === 'red'
next(); // 'green'
next(); // 'blue'
next(); // wraps -> 'red'
const [tab, { prev, setIndex }] = useCycleList(['a', 'b', 'c'], 0);
prev(); // wraps back -> 'c'
setIndex(4); // 4 % 3 === 1 -> 'b'
setIndex(-1); // -> last item 'c'
next past the end returns to index 0; prev before 0 returns to the last index. Modular arithmetic, not clamping.setIndex normalizes — any integer maps into [0, length) via wraparound, so negatives and overshoots are valid.next/prev/setIndex should keep the same identity across renders (wrap them in useCallback) so consumers can pass them to memoized children.items has at least one element.You'll hold an index in state and expose stable next/prev/setIndex controls that move it around the array using modular arithmetic, so the ends wrap.
A list you cycle through is really a ring: after the last item comes the first again, and before the first comes the last. The only state you need is a single index; the current item is just items[index]. The whole job is moving that index correctly — +1 and -1 that wrap instead of falling off the array, plus a jump-to-index that tolerates any integer. Carousels, theme rotators, and "next slide" buttons are all this hook.
Picture the indices arranged in a circle, not a line. Moving forward from the last position doesn't hit a wall — it loops to 0. The tool for "wrap around a fixed range" is the modulo operator: (i + 1) % length keeps you in [0, length). The one wrinkle is going backward: (i - 1) % length can go negative in JavaScript, so you add length first to keep it positive.
The obvious version increments and decrements without wrapping:
function useCycleListNaive(items, startIndex = 0) {
const [index, setIndex] = useState(startIndex);
const active = items[index];
const next = () => setIndex(index + 1);
const prev = () => setIndex(index - 1);
return [active, { index, next, prev, setIndex }];
}
Three problems. next at the last item sets index to items.length, so items[index] is undefined — it falls off the end instead of wrapping. prev at 0 sets -1, also undefined. And next/prev are recreated every render and close over a stale index, so a memoized child sees a new function each time and calling next() twice in one tick both read the same index. We need modulo for the wrap and a functional updater for correctness.
const { useState, useCallback } = require('react');
function useCycleList(items, startIndex = 0) {
const len = items.length;
const [index, setIndex] = useState(startIndex);
// Normalize ANY integer into [0, len) with wraparound.
const goTo = useCallback(
(i) => setIndex(((i % len) + len) % len),
[len],
);
// Use functional updaters so rapid calls compose off the latest index.
const next = useCallback(() => setIndex((i) => (i + 1) % len), [len]);
const prev = useCallback(() => setIndex((i) => (i - 1 + len) % len), [len]);
return [items[index], { index, next, prev, setIndex: goTo }];
}
module.exports = { useCycleList };
The fixes are all in the arithmetic and the identities. next uses (i + 1) % len so the last index rolls to 0; prev uses (i - 1 + len) % len — the + len guards against JavaScript's negative modulo so index 0 rolls to len - 1. goTo (exposed as setIndex) applies ((i % len) + len) % len, the standard "positive modulo" idiom, so any integer — 4, -1, 100 — maps into range. Every control is wrapped in useCallback keyed on len, giving them stable identities across renders, and next/prev take functional updaters so two calls in the same tick each build on the latest index rather than a captured stale one.
Take useCycleList(['red', 'green', 'blue']) — len is 3, index starts at 0, active is 'red':
next() — setIndex(i => (0 + 1) % 3) = 1. Active becomes items[1] = 'green'.next() again — (1 + 1) % 3 = 2 → 'blue'.next() again — (2 + 1) % 3 = 0 → back to 'red'. The end wrapped instead of going undefined.prev() — (0 - 1 + 3) % 3 = 2 → 'blue'. Going before the start wrapped to the last item.setIndex(4) — ((4 % 3) + 3) % 3 = 1 → 'green'. Out-of-range input normalized.The current item is always items[index]; every control just moves index around the ring.
index + 1 past the end yields undefined. Wrap with % length.-1 % 3 is -1, not 2. Add length before the modulo when going backward or normalizing arbitrary input.next/prev — reading index from the render scope captures an old value; use a functional updater setIndex(i => ...).useCallback, memoized children re-render and effect deps churn. Key the callbacks on length.setInterval (cleared on unmount) turns it into an auto-playing carousel; next is already the tick handler.items mid-cycle — if the array shrinks, the current index can point past the new end; clamping or re-normalizing index when length changes keeps active valid.useCycle — the same idea in a popular library, returning [state, cycle] where cycle() advances and cycle(i) jumps; useful to recognize the pattern in the wild.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Lots of UI is a ring: a carousel that loops, a theme toggle that rotates light → dark → system → light, a "next tip" button that never runs out. useCycleList captures that pattern — hold an index into an array and move it forward or backward with wraparound, so stepping off the end lands back at the start and stepping before the start lands on the last item.
Implement useCycleList(items, startIndex = 0). It returns [activeItem, controls], where controls exposes the current index plus next(), prev(), and setIndex(i) — all of which keep the index inside the array's bounds by wrapping.
function useCycleList(items, startIndex = 0) {
// returns [activeItem, { index, next, prev, setIndex }]
}
const [color, { next }] = useCycleList(['red', 'green', 'blue']);
// color === 'red'
next(); // 'green'
next(); // 'blue'
next(); // wraps -> 'red'
const [tab, { prev, setIndex }] = useCycleList(['a', 'b', 'c'], 0);
prev(); // wraps back -> 'c'
setIndex(4); // 4 % 3 === 1 -> 'b'
setIndex(-1); // -> last item 'c'
next past the end returns to index 0; prev before 0 returns to the last index. Modular arithmetic, not clamping.setIndex normalizes — any integer maps into [0, length) via wraparound, so negatives and overshoots are valid.next/prev/setIndex should keep the same identity across renders (wrap them in useCallback) so consumers can pass them to memoized children.items has at least one element.You'll hold an index in state and expose stable next/prev/setIndex controls that move it around the array using modular arithmetic, so the ends wrap.
A list you cycle through is really a ring: after the last item comes the first again, and before the first comes the last. The only state you need is a single index; the current item is just items[index]. The whole job is moving that index correctly — +1 and -1 that wrap instead of falling off the array, plus a jump-to-index that tolerates any integer. Carousels, theme rotators, and "next slide" buttons are all this hook.
Picture the indices arranged in a circle, not a line. Moving forward from the last position doesn't hit a wall — it loops to 0. The tool for "wrap around a fixed range" is the modulo operator: (i + 1) % length keeps you in [0, length). The one wrinkle is going backward: (i - 1) % length can go negative in JavaScript, so you add length first to keep it positive.
The obvious version increments and decrements without wrapping:
function useCycleListNaive(items, startIndex = 0) {
const [index, setIndex] = useState(startIndex);
const active = items[index];
const next = () => setIndex(index + 1);
const prev = () => setIndex(index - 1);
return [active, { index, next, prev, setIndex }];
}
Three problems. next at the last item sets index to items.length, so items[index] is undefined — it falls off the end instead of wrapping. prev at 0 sets -1, also undefined. And next/prev are recreated every render and close over a stale index, so a memoized child sees a new function each time and calling next() twice in one tick both read the same index. We need modulo for the wrap and a functional updater for correctness.
const { useState, useCallback } = require('react');
function useCycleList(items, startIndex = 0) {
const len = items.length;
const [index, setIndex] = useState(startIndex);
// Normalize ANY integer into [0, len) with wraparound.
const goTo = useCallback(
(i) => setIndex(((i % len) + len) % len),
[len],
);
// Use functional updaters so rapid calls compose off the latest index.
const next = useCallback(() => setIndex((i) => (i + 1) % len), [len]);
const prev = useCallback(() => setIndex((i) => (i - 1 + len) % len), [len]);
return [items[index], { index, next, prev, setIndex: goTo }];
}
module.exports = { useCycleList };
The fixes are all in the arithmetic and the identities. next uses (i + 1) % len so the last index rolls to 0; prev uses (i - 1 + len) % len — the + len guards against JavaScript's negative modulo so index 0 rolls to len - 1. goTo (exposed as setIndex) applies ((i % len) + len) % len, the standard "positive modulo" idiom, so any integer — 4, -1, 100 — maps into range. Every control is wrapped in useCallback keyed on len, giving them stable identities across renders, and next/prev take functional updaters so two calls in the same tick each build on the latest index rather than a captured stale one.
Take useCycleList(['red', 'green', 'blue']) — len is 3, index starts at 0, active is 'red':
next() — setIndex(i => (0 + 1) % 3) = 1. Active becomes items[1] = 'green'.next() again — (1 + 1) % 3 = 2 → 'blue'.next() again — (2 + 1) % 3 = 0 → back to 'red'. The end wrapped instead of going undefined.prev() — (0 - 1 + 3) % 3 = 2 → 'blue'. Going before the start wrapped to the last item.setIndex(4) — ((4 % 3) + 3) % 3 = 1 → 'green'. Out-of-range input normalized.The current item is always items[index]; every control just moves index around the ring.
index + 1 past the end yields undefined. Wrap with % length.-1 % 3 is -1, not 2. Add length before the modulo when going backward or normalizing arbitrary input.next/prev — reading index from the render scope captures an old value; use a functional updater setIndex(i => ...).useCallback, memoized children re-render and effect deps churn. Key the callbacks on length.setInterval (cleared on unmount) turns it into an auto-playing carousel; next is already the tick handler.items mid-cycle — if the array shrinks, the current index can point past the new end; clamping or re-normalizing index when length changes keeps active valid.useCycle — the same idea in a popular library, returning [state, cycle] where cycle() advances and cycle(i) jumps; useful to recognize the pattern in the wild.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.