The question is published on by Tutorial Guruji team.
I’ve been trying to show every step of the sorting process of an array using react. I’ve implemented the bubble sort algorithm, but I noticed a very strange thing:
When I using this approach UI instantly show a sorted array:
function BubbleSort({ array }) { const [elements, setElements] = useState(array); const onSort = () => { console.log('onSort fired') const length = elements.length; const newElements = elements.slice(); let swap = 0; for (let i = 0; i < length; i++) { for (let j = i + 1; j < length; j++) { if (newElements[i].value > newElements[j].value) { console.log('Swap fired', swap); const buffer = newElements[i]; newElements[i] = newElements[j]; newElements[j] = buffer; swap++ setTimeout(() => setElements(newElements.slice()), 1000 * swap); } } } } return ( <ArrayView elements={elements} onSort={onSort}/> ); }
But, If I change my code a little bit It works correctly:
import React, {useState} from "react"; import ArrayView from "../ArrayView/ArrayView"; function BubbleSort({ array }) { const events = []; const [elements, setElements] = useState(array); const onSort = () => { console.log('onSort fired') const length = elements.length; const newElements = elements.slice(); let swap = 0; for (let i = 0; i < length; i++) { for (let j = i + 1; j < length; j++) { if (newElements[i].value > newElements[j].value) { console.log('Swap fired', swap); const buffer = newElements[i]; newElements[i] = newElements[j]; newElements[j] = buffer; swap++ events.push(newElements.slice()); } } } for (let i = 0; i < events.length; i++) { setTimeout(() => setElements(events[i]), 1000 * i); } } return ( <ArrayView elements={elements} onSort={onSort}/> ); } export default BubbleSort;
In fact, this is exactly the same code. Why does it happen?
Answer
setTimeout
is non-blocking.
In your first code example, you set a timeout after each step of the sorting process. However, that doesn’t cause the code to pause and wait, it schedules the callback to occur later. So your code continues and performs the next step of the sort, and then schedules another callback, and so on. After 1 second has passed, the callbacks fire, but the array has already finished being sorted, so the UI changes to a fully sorted array.
In your second example, you store the steps and space the timeouts to fire appropriately. The callbacks don’t look at the global state of the array, they look at each event and play them back as expected.
EDIT: Addressing your comments.
You are correct that calling slice
creates a copy of the array. The problem is that you are calling slice
after the callback is scheduled.
When you create an arrow function, the variables from its scope are captured, but the code inside is not evaluated until the function is called. In this case, 1 second will go by and then it will call newElements.slice()
and pass that to setElements
. At that point, newElements
has already been sorted and all your copies will look the same.
You can solve that in two ways. The first is to do what you mentioned in the comments. Personally, I find that hard to read. The other way is to call slice
immediately and use that value in the callback.
const newElementsCopy = newElements.slice(); setTimeout(() => setElements(newElementsCopy), 1000 * swap);