DEV Community

gaven yang
gaven yang

Posted on

Stop Using `Math.random()` for Raffles: Building a Truly Fair Wheel with React & Web Crypto API

 We've all built that simple random picker for a side project using Math.random(). It's easy, it's one line of code, and it works... mostly.

But when I was building JoySpin – a tool designed for teachers and live event raffles – I realized that "mostly" wasn't good enough. If a teacher uses my tool to pick a student for a prize, or a streamer uses it to give away a gift card, the randomness needs to be cryptographically secure.

Here is why Math.random() fails for high-stakes picking, and how I implemented the Web Crypto API in React to fix it.

The Problem: Pseudo-Randomness

Math.random() is a Pseudo-Random Number Generator (PRNG). It’s fast, but it’s deterministic. If you know the internal state of the algorithm (like V8's XorShift128+), you can theoretically predict the next number.

For a UI animation? Fine.
For picking a winner? Not ideal.

The Solution: window.crypto

To make JoySpin truly fair, I switched to window.crypto.getRandomValues(). This taps into the operating system's entropy pool (mouse movements, keystrokes, thermal noise) to generate true randomness.

Here is the React hook I wrote to handle the selection logic:

// A safe random number generator function
const getSecureRandomNumber = (min, max) => {
  const range = max - min + 1;
  const maxSafeInteger = Math.pow(2, 32) - 1;
  // Determine the largest number that is a multiple of range
  const limit = maxSafeInteger - (maxSafeInteger % range);
  const randomBuffer = new Uint32Array(1);

  let randomNumber;
  do {
    window.crypto.getRandomValues(randomBuffer);
    randomNumber = randomBuffer[0];
  } while (randomNumber >= limit); // Reject numbers to avoid bias (modulo bias)

  return min + (randomNumber % range);
};
Enter fullscreen mode Exit fullscreen mode

Handling the "Spin" State in React

The logic is only half the battle. In JoySpin, the UX is just as important. Users need to feel the weight of the wheel.

I separated the logic from the animation. The "Winner" is decided the moment the button is clicked (using the crypto function above), but the wheel visual is CSS logic that eases out to that specific degree.

  const handleSpin = () => {
    if (spinning) return;

    // 1. Decide the winner immediately using Crypto API
    const winningIndex = getSecureRandomNumber(0, currentList.length - 1);
    const winner = currentList[winningIndex];

    // 2. Calculate rotation (add extra spins for suspense)
    const newRotation = rotation + 1440 + (360 / currentList.length) * winningIndex;

    // 3. Update State
    setWinner(winner);
    setRotation(newRotation);
    // ... animation logic
  };
Enter fullscreen mode Exit fullscreen mode

The Result: A "Trustless" Picker

By implementing this, I can tell users that the result isn't just a JavaScript quirk—it's mathematically fair.

I built this into a full production app, JoySpin.xyz. It runs entirely in the browser (no server-side bias), supports importing lists, and includes an "Instant Elimination" mode to remove winners after they are picked.

If you are building a game or a raffle tool, I highly recommend ditching Math.random(). The performance cost is negligible, but the trust you gain from users is worth it.

Have you ever dealt with "randomness" complaints in your apps? Let me know in the comments!

Top comments (0)