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.

Round 1 / 3score 0

Before the reveal — what does 2 + 2 evaluate to?

Customize
Summary

Installation

npx shadcn@latest add https://craftbits.dev/r/use-predict-rounds.json

shadcn'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

ParameterTypeDescription
roundsreadonly PredictRound<T>[]The sequence of rounds. Each is { id: string; correctValue: T }.
options.initialIndexnumberStarting index. Defaults to 0. Clamped to [0, rounds.length].
options.onComplete(result: PredictRoundsResult<T>) => voidFires once when next() is called from the last round. Receives { score, total, history }.

Return value

FieldTypeDescription
currentRoundPredictRound<T> | nullThe active round, or null once finished.
currentIndexnumberZero-based index. Equal to total once finished.
totalnumberrounds.length.
predictionT | nullThe student's prediction for the current round.
revealedbooleanWhether the current round has been revealed.
scorenumberCumulative correct predictions across revealed rounds.
predict(value: T) => voidRecord the student's prediction. No-op after reveal or when finished.
reveal() => voidMark the current round revealed.
next() => voidAdvance. Fires onComplete on the last round.
reset() => voidReturn to the initial state.
historyreadonly PredictRoundHistoryEntry<T>[]Per-round history (length always equals rounds.length).

Behaviour

  1. State per round. Predictions and revealed-flags are stored in Map / Set keyed by round.id, not by index, so re-ordering or replacing the rounds prop preserves answers for surviving ids.
  2. Locked after reveal. Once a round is revealed, predict(value) is a no-op — locking the answer in is the point of the pattern.
  3. Score counts only revealed-correct rounds. Skipped rounds leave correct: null in history and never contribute to the score.
  4. Finished state. When next() is called from the last round, currentRound becomes null and onComplete fires once via queueMicrotask so consumers' state updates from the same render commit before they observe the completion.
  5. 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", and aria-live feedback.
  • Announce round transitions. Screen-reader users benefit from a short aria-live="polite" heading (e.g. Round 2 of 3). The hook exposes currentIndex and total for 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, separates predict / reveal / next (the source coupled reveal + advance), and exposes onComplete for end-of-sequence analytics.