jeudi 12 août 2021

React: random number out of nowhere - when shuffling arrays, useState callback getting called more times than expected

I have this code that generates a shuffle map to display children in a random order:

import React from "react"

function shuffle(array) {
  let currentIndex = array.length
  let randomIndex

  // While there remain elements to shuffle...
  while (0 !== currentIndex) {

    // Pick a remaining element...
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;
    
    // And swap it with the current element.
    [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
  }
  return array;
}


let lastShuffle
function generateShuffleMap(l) {
  if (l === 0) return [];
  if (l === 1) return [0];

  let arr = new Array(l)
  for (let i = 0; i < arr.length; ++i) {
    arr[i] = i
  }
  let r = Math.random()
  console.log("ShuffleChildren randomNumber", r)
  arr.randomNumber = r

  while (true) {
    shuffle(arr)
    
    if (lastShuffle === undefined) {
      console.log("ShuffleChildren lastShuffle undefined", arr, lastShuffle)
      return arr
    }
    
    for (let i = 0; i < arr.length; ++i) {
      console.log("ShuffleChildren compare", arr[i], lastShuffle[i])
      if (arr[i] !== lastShuffle[i]) {
        console.log("ShuffleChildren found mismatch!", arr, arr.randomNumber, lastShuffle, lastShuffle.randomNumber)
        return arr
      }
    }
    
    // Identical shuffle. Try again.
    console.log("ShuffleChildren try again")
  }
}

export default function ShuffleChildren(props) {
  const [map, setMap] = React.useState(()=>{
    let m = generateShuffleMap(props.children.length)
    lastShuffle = m
    console.log("ShuffleChildren memo", m, m.randomNumber)
    //shuffleMapTest(2, 60)
    return m
  })
  console.log("ShuffleChildren render", map, map.randomNumber)
  return React.Children.toArray(props.children).map((v, k) => props.children[map[k]])
}

Sounds pretty simple. It makes sure you don't get the same shuffle twice by trying again. So a series of length-2 shuffles would be [0, 1], [1, 0], [0, 1], etc. But here's my output:

ShuffleChildren randomNumber 0.5832622841674447
ShuffleChildren lastShuffle undefined Array [ 1, 0 ] undefined
ShuffleChildren memo Array [ 1, 0 ] 0.5832622841674447
ShuffleChildren render Array [ 1, 0 ] 0.5832622841674447
ShuffleChildren randomNumber 0.396479700829085
ShuffleChildren compare 0 0
ShuffleChildren compare 1 1
ShuffleChildren try again
ShuffleChildren compare 1 0
ShuffleChildren found mismatch! Array [ 1, 0 ] 0.396479700829085 Array [ 0, 1 ] 0.7683400651535296
ShuffleChildren memo Array [ 1, 0 ] 0.396479700829085
ShuffleChildren render Array [ 1, 0 ] 0.396479700829085
ShuffleChildren randomNumber 0.4286083485990334
ShuffleChildren compare 1 0
ShuffleChildren found mismatch! Array [ 1, 0 ] 0.4286083485990334 Array [ 0, 1 ] 0.6164973053401377
ShuffleChildren memo Array [ 1, 0 ] 0.4286083485990334
ShuffleChildren render Array [ 1, 0 ] 0.4286083485990334

I'm getting [1, 0] three times in a row. And what's more, on found mismatch! lastShuffle is an array with a random number I didn't even generate! I wrote some tests to investigate:

function shuffleTest(l, n) {
  let permutations = {}
  for (let i = 0; i < n; ++i) {
    let arr = new Array(l)
    for (let i = 0; i < arr.length; ++i) {
      arr[i] = i
    }
    arr = shuffle(arr)
    let join = arr.join(',')
    permutations[join] = permutations[join] || 0
    permutations[join]++ 
  }
  console.log(`ShuffleChildren shuffleTest(${l}, ${n})`, permutations)
}

function shuffleMapTest(l, n) {
  let permutations = {}
  for (let i = 0; i < n; ++i) {
    let arr = generateShuffleMap(l)
    lastShuffle = arr
    let join = arr.join(',')
    permutations[join] = permutations[join] || 0
    permutations[join]++ 
  }
  console.log(`ShuffleChildren shuffleMapTest(${l}, ${n})`, permutations)
}

But the individual functions themselves seem to be functioning.

ShuffleChildren shuffleTest(2, 100) Object { "0,1": 53, "1,0": 47 }
ShuffleChildren shuffleTest(2, 1000) Object { "1,0": 492, "0,1": 508 }
ShuffleChildren shuffleMapTest(2, 100) Object { "1,0": 50, "0,1": 50 }
ShuffleChildren shuffleMapTest(2, 1000) Object { "1,0": 500, "0,1": 500 }

What is even going on here? How could randomNumber have changed without me knowing about it?




Aucun commentaire:

Enregistrer un commentaire