I have an async function which I used in sorting visualizer. I want to add a feature of pause and unpause. I tried to use a while(isTrue){} (isTrue is a usestate variable) but this method is making the page unresponsive. Is there any better way to add this feature?
import asyncSetTimeout from '../helpers/asyncSetTimeout';
const bubbleSort = async ({
array,
setArray,
setColorsArray,
visualizationSpeed,
setI,
setJ,
setNum1,
setNum2,
comparisons,
setComparisons,
swaps,
setswaps,
isTrue
} = {}) => {
comparisons=0;
swaps=0;
let len = array.length;
for (let i = 0; i < len - 1; i++) {
setI(i);
for (let j = 0; j < len - 1 - i; j++) {
setJ(j);
let newColorsArray = new Array(len).fill(0);
newColorsArray[len - 1 - i] = 3;
newColorsArray[j] = 1;
newColorsArray[j + 1] = 2;
setColorsArray(newColorsArray);
await asyncSetTimeout({
timeout: 10 * visualizationSpeed
});
setNum1(array[j]);
setNum2(array[j + 1]);
comparisons++;
setComparisons(comparisons)
if (array[j + 1] < array[j]) {
let temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
swaps++;
setswaps(swaps)
setArray(array);
}
await asyncSetTimeout({
timeout: 10 * visualizationSpeed
})
while(isTrue){}
console.log(isTrue);
}
}
setColorsArray([])
};
export default bubbleSort;
start from basics
I would recommend a generator -
function* bubbleSort(array) {
yield array
let swapping = true
while (swapping) {
swapping = false
for (let i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
[array[i], array[i + 1]] = [array[i + 1], array[i]]
swapping = true
yield array
}
}
}
}
Now you can use for..of
to step through the generator, displaying one step at a time -
const array = [5, 3, 1, 4, 2];
for (const step of bubbleSort(array)) {
console.log(step); // ✅ display one unit of progress
}
To add pause
behaviour, we can define a controller that allows us to interrupt, pause,
let controller = // ... ✅
const array = [5, 3, 1, 4, 2];
for (const step of bubbleSort(array)) {
if (controller.cancelled) break // ✅
await controller.promise // ✅
console.log(step);
}
controller.pause() // ✅ pause action
controller.unpause() // ✅ unpause action
controller.cancel() // ✅ cancel action
Implementation of the controller
might look like this -
const controller = {
cancelled: false,
promise: null,
pause() {
this.unpause()
this.promise = new Promise(r => this.unpause = r)
},
unpause() {},
cancel() {
this.cancelled = true
}
}
vanilla demo
Run the demo and verify the result -
function* bubbleSort(array) {
yield array
let swapping = true
while (swapping) {
swapping = false
for (let i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
[array[i], array[i + 1]] = [array[i + 1], array[i]]
swapping = true
yield array
}
}
}
}
async function main(form, arr, controller) {
for (const sortedArray of bubbleSort(arr)) {
if (controller.cancelled) break
await controller.promise
form.output.value += JSON.stringify(sortedArray) + "\n"
await sleep(700)
}
form.output.value += "done\n"
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
const array = [5, 8, 3, 6, 1, 7, 4, 2]
const controller = {
cancelled: false,
promise: null,
pause() {
this.unpause()
this.promise = new Promise(r => this.unpause = r)
},
unpause() {},
cancel() {
this.cancelled = true
}
}
const f = document.forms[0]
f.pause.addEventListener("click", e => controller.pause())
f.unpause.addEventListener("click", e => controller.unpause())
f.cancel.addEventListener("click", e => controller.cancel())
main(f, array, controller).catch(console.error)
<form>
<button type="button" name="pause">pause</button>
<button type="button" name="unpause">unpause</button>
<button type="button" name="cancel">cancel</button>
<pre><output name="output"></output></pre>
</form>
react
To make this compatible with React, we have to make a few changes. Most notably, bubbleSort
should not mutate its input. But we'll also yield
the indices which have changed at each step so we can create a better visualization -
App preview |
---|
![]() |
bubbleSort React demo |
function* bubbleSort(t) {
yield [t, -1, -1] // ✅ yield starting point
let swapping = true
while (swapping) {
swapping = false
for (let i = 0; i < t.length- 1; i++) {
if (t[i] > t[i + 1]) {
t = [ //
...t.slice(0, i), //
t.at(i + 1), // ✅ immutable swap
t.at(i), //
...t.slice(i + 2) //
]
swapping = true
yield [t, i, i + 1] // ✅ yield array and indices
}
}
}
}
The controller is moved into a useController
hook -
function useController() {
return React.useMemo(
() => ({
cancelled: false,
promise: null,
pause() {
this.unpause()
this.promise = new Promise(r => this.unpause = r)
},
unpause() {},
cancel() {
this.cancelled = true
}
}),
[] // no dependencies
)
}
Finally the component has steps
and done
state, a controller
instance, and an effect to step through the generator. Note the careful use of mounted
to ignore stale component updates -
function App({ unsortedArray = [] }) {
const [steps, setSteps] = React.useState([])
const [done, setDone] = React.useState(false)
const controller = useController()
React.useEffect(() => {
let mounted = true
async function main() {
mounted && setDone(false)
mounted && setSteps([])
for (const step of bubbleSort(unsortedArray)) {
if (controller.cancelled) break
await controller.promise
mounted && setSteps(s => [...s, step])
await sleep(700)
}
}
main()
.then(() => mounted && setDone(true))
.catch(console.error)
return () => { mounted = false }
}, [unsortedArray, controller])
return <div>
<button onClick={e => controller.pause()} children="pause" />
<button onClick={e => controller.unpause()} children="unpause" />
<button onClick={e => controller.cancel()} children="cancel" />
<pre>
{steps.map(([data, i, j]) =>
<>
[ {data.map((n, index) =>
index == i || index == j
? <span style={{color: "red", textDecoration: "underline"}}>{n}, </span>
: <span style={{color: "silver"}}>{n}, </span>
)}]{"\n"}
</>
)}
{done && "done"}
</pre>
</div>
}