Here is my hella' over-complicated solution: https://codesandbox.io/s/kind-leavitt-z7hq1
Basically I created a generator to shuffle and iterate the array. Then inside my react component I have a state that stores the current value, and a callback to advance the iterator.
Only caveat is the array instance mustn't change unless you want to start the iteration over. What I mean by this is that you shouldn't pass an array expression to the component as part of a render function. Because during each render, the instance will be new. So make sure that your array is set somewhere statically.
import React, { useState, useMemo, useCallback } from "react";
import "./styles.css";
/**
* Shuffles array in place. ES6 version
* https://stackoverflow.com/a/6274381/701263
* @param {Array} a items An array containing the items.
*/
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// The * denotes a generator function
function* shuffleIterator(array) {
// Shuffle the array
const shuffled = shuffle(array);
// Iterate it
for (let item of shuffled) {
// Yield the item out
yield item;
}
}
const MyComponent = ({ array }) => {
/**
* Get our iterator, we use memo so that the iterator doesn't
* get created every render. Only when array changes will the
* iterator be re-created.
**/
const iter = useMemo(() => shuffleIterator(array), [array]);
// This callback will get the next yielded value from the iterator.
const getNextState = useCallback(() => {
const next = iter.next();
// If done, return false
if (next.done) {
return false;
} else {
// else return the value
return next.value;
}
}, [iter]);
/**
* Setup our item state, telling React to get the intitial
* value from getNextState.
**/
const [item, setItem] = useState(getNextState);
// Create a callback to be called on our button click
const next = useCallback(() => {
// Set the item to the next yieled value
setItem(getNextState());
}, [getNextState]);
// if the value is false, return done
if (item === false) {
return <div>Done!</div>;
} else {
// Else return our value and a button to advance
return (
<div>
<span>{item}</span>
<button onClick={next}>Next</button>
</div>
);
}
};
/**
* NOTE: This array is defined here and passed in to MyComponent.
* If I had defined the array inside App the instance would be new
* each time and the app would iterate forever.
**/
const myArray = [1, 2, 3, 4, 5];
export default function App() {
return <MyComponent array={myArray} />;
}