usePredictRounds
A React hook that turns a list of predict-then-reveal rounds into a tiny state machine. You provide rounds (each with an id and a correctValue); the hook tracks which round is current, what the student predicted, whether the answer has been revealed, the running score, and a per-round history.
It pairs naturally with PredictionGate — one gate per round, the hook drives the gate's controlled surface, and next() advances. Reach for it when you want multiple guided "guess before peeking" checkpoints in an article or lesson.
Before the reveal — what does 2 + 2 evaluate to?
Installation
npx shadcn@latest add https://craftbits.dev/r/use-predict-rounds.jsonshadcn's CLI resolves registry dependencies transitively, so installing usePredictRounds also pulls in PredictionGate (and its dependency chain — OptionPicker, LessonButton). No external npm dependencies.
Usage
"use client";
import { usePredictRounds } from "@craft-bits/core";
import { PredictionGate } from "@craft-bits/core/buttons/prediction-gate";
const rounds = [
{ id: "q1", correctValue: "4" },
{ id: "q2", correctValue: "56" },
];
const options = [
{ value: "4", label: "4" },
{ value: "5", label: "5" },
{ value: "56", label: "56" },
{ value: "63", label: "63" },
];
export function Quiz() {
const r = usePredictRounds(rounds);
if (r.currentRound === null) {
return <p>Score: {r.score} / {r.total}</p>;
}
return (
<PredictionGate
prompt={"Predict the answer."}
options={options}
correctValue={r.currentRound.correctValue}
value={r.prediction}
onValueChange={(v) => v !== null && r.predict(v)}
revealed={r.revealed}
onRevealedChange={(rev) => rev && r.reveal()}
/>
);
}API
Parameters
| Parameter | Type | Description |
|---|---|---|
rounds | readonly PredictRound<T>[] | The sequence of rounds. Each is { id: string; correctValue: T }. |
options.initialIndex | number | Starting index. Defaults to 0. Clamped to [0, rounds.length]. |
options.onComplete | (result: PredictRoundsResult<T>) => void | Fires once when next() is called from the last round. Receives { score, total, history }. |
Return value
| Field | Type | Description |
|---|---|---|
currentRound | PredictRound<T> | null | The active round, or null once finished. |
currentIndex | number | Zero-based index. Equal to total once finished. |
total | number | rounds.length. |
prediction | T | null | The student's prediction for the current round. |
revealed | boolean | Whether the current round has been revealed. |
score | number | Cumulative correct predictions across revealed rounds. |
predict | (value: T) => void | Record the student's prediction. No-op after reveal or when finished. |
reveal | () => void | Mark the current round revealed. |
next | () => void | Advance. Fires onComplete on the last round. |
reset | () => void | Return to the initial state. |
history | readonly PredictRoundHistoryEntry<T>[] | Per-round history (length always equals rounds.length). |
Behaviour
- State per round. Predictions and revealed-flags are stored in
Map/Setkeyed byround.id, not by index, so re-ordering or replacing the rounds prop preserves answers for surviving ids. - Locked after reveal. Once a round is revealed,
predict(value)is a no-op — locking the answer in is the point of the pattern. - Score counts only revealed-correct rounds. Skipped rounds leave
correct: nullin history and never contribute to the score. - Finished state. When
next()is called from the last round,currentRoundbecomesnullandonCompletefires once viaqueueMicrotaskso consumers' state updates from the same render commit before they observe the completion. - SSR-safe. No globals. State lives in the component.
Examples
Numeric values
const rounds = [
{ id: "double-21", correctValue: 42 },
{ id: "double-50", correctValue: 100 },
];
const r = usePredictRounds<number>(rounds);
// r.prediction and r.history[i].prediction are `number | null`.With analytics
const r = usePredictRounds(rounds, {
onComplete: ({ score, total }) => {
track("quiz-complete", { score, total });
},
});Props
usePredictRounds is a hook. Its parameter surface is documented under API above; it takes no JSX-style props.
Accessibility
usePredictRounds is a headless utility — it has no DOM surface and no accessibility implications of its own. Two notes for consumers:
- Pair with
PredictionGate. That primitive owns the visible label, focus management,role="form", andaria-livefeedback. - Announce round transitions. Screen-reader users benefit from a short
aria-live="polite"heading (e.g.Round 2 of 3). The hook exposescurrentIndexandtotalfor exactly this — render them inside a live region above the gate.
Credits
- Extracted from:
algoflashcards(src/platform/hooks/state/usePredictRounds.ts). The library version generalises over the round value type, adds named-round history, separatespredict/reveal/next(the source coupled reveal + advance), and exposesonCompletefor end-of-sequence analytics.