Search code examples
reactjssortingreact-hookssettimeoutvisualization

How can I using state instead of setTimeout?


I am learning react and decided to try create a sorting visualizer. I started with bubble sort and pretty much succeeded creating a basic visualizer. setTimeout was the main function that I used to apply the visualization.

I felt that relaying on setTimeout does not utilize react enough and I wanted to try different approach, applying the visualization with useState hook and the rerendering that is happening when changing the state. I understand that useState hook is asynchronous, and will not immediately reflect.

Here is my code:

import React, { useContext, useState, useEffect } from 'react';

const NUMBER_OF_ELEMENTS = 10;

const DEFAULT_COLOR = 'black';

const randomIntFromInterval = (min, max) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

const Dummy = () => {
    const [arr, setArr] = useState([]);
    const [numberOfElements, setNumberOfElements] = useState(NUMBER_OF_ELEMENTS);
    const [doneElements, setDoneElements] = useState([]);

    useEffect(() => {
        resetArray();
    }, []);

    const resetArray = () => {
        const arr1 = [];
        for(let i = 0; i < numberOfElements; i++)
        {
            arr1[i] = randomIntFromInterval(5, 100);
        }
        console.log(arr1);
        setArr(arr1);
    }

    const bubbleSort = (arr, n) => {
        let i, j, temp, swapped, delay = 1;
        for(i = 0; i < n - 1; i++) 
        {
            swapped = false;
            for(j = 0; j < n - i - 1; j++) 
            {
                createColor(j, j + 1, delay++, 'darkred');
                if(arr[j] > arr[j + 1]) 
                {
                    // swap arr[j] and arr[j+1] 
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                    createAnimation(j, j + 1, delay++);
                }
                createColor(j, j + 1, delay++, 'black');
            }
            createSingleColor(n - i - 1, delay++, 'green');

            // If no two elements were  
            // swapped by inner loop, then break 
            if(swapped === false) break;
        }

        for(let k = 0; k < n - i - 1; k++) {
            createSingleColor(k, delay++, 'green');
        }
    }

    const createAnimation = (one, two, delay) => {
        const arrayBars = document.getElementsByClassName('array-bar');
        setTimeout(() => {
            const barOneHeight = arrayBars[one].style.height;
            const barTwoHeight = arrayBars[two].style.height;
            arrayBars[two].style.height = `${barOneHeight}`;
            arrayBars[one].style.height = `${barTwoHeight}`;
        }, 250 * delay);
    }

    const createColor = (one, two, delay, color) => {
        const arrayBars = document.getElementsByClassName('array-bar');
        setTimeout(() => {
            arrayBars[two].style.backgroundColor = color;
            arrayBars[one].style.backgroundColor = color;
        }, 250 * delay); 
    }

    const createSingleColor = (index, delay, color) => {
        const arrayBars = document.getElementsByClassName('array-bar');
        setTimeout(() => {
            arrayBars[index].style.backgroundColor = color;
        }, 250 * delay); 
    }

    const handleSort = (arr) => {
        bubbleSort(arr, arr.length);
    }

    const handlerRange = (e) => {
        setNumberOfElements(e.target.value);
    }

    return (
        <div>
            <div className="array-container">
                {arr.map((value, idx) => (
                    <div className="array-bar"
                         key={idx}
                         style={{
                            backgroundColor: 'black',
                            height: `${value}px`,
                            width: `${100 / arr.length}%`,
                            display: 'inline-block',
                            margin: '0 1px'
                         }}>
                    </div>
                ))}
            </div>

            <div className="buttons-container">
                <button onClick={() => handleSort(arr)}>Sort!</button>
                <button onClick={() => resetArray()}>Reset</button>
                <button onClick={() => {
                    setDoneElements([...doneElements, 7]);
                    console.log(doneElements);}}>print</button>
            </div>
            <div className="slider-container">
                1
                <input type="range" 
                       min="1" 
                       max="100" 
                       onChange={(e) => handlerRange(e)} 
                       className="slider" 
                       id="myRange" 
                />
                100
            </div>
            {numberOfElements}

        </div>
    );
}

export default Dummy;

For example when I tried using the setDoneElements in the bubblesort function I messed up the visualization.

Is there a way to use hooks to apply the visualization, and not to rely on setTimeout that much?


Solution

  • Found a solution:

    import React, { useState, useEffect } from 'react';
    const shortid = require('shortid');
    
    const randomIntFromInterval = (min, max) => {
        return Math.floor(Math.random() * (max - min + 1) + min);
    }
    
    const Dummy2 = () => {
        const [arr, setArr] = useState([]);
        const [length, setLength] = useState(10);
        const [doneElements, setDoneElements] = useState([]);
        const [compareElements, setCompareElements] = useState([]);
    
        useEffect(() => {
            generateArray();
        }, [length]);
    
        const generateArray = () => {
            setDoneElements([]);
            setCompareElements([]);
            const tempArr = [];
            for(let i = 0; i < length; i++) {
                tempArr.push(randomIntFromInterval(7, 107));
            }
            setArr([...tempArr]);
        }
    
        const handleLength = (e) => {
            setLength(e.target.value);
        }
    
        const bubbleSort = () => {
            let i, j, swapped, delay = 100;
            const tempArr = [...arr];
            const tempDoneElements = [...doneElements];
            for(i = 0; i < length - 1; i++) 
            {
                swapped = false;
                for(j = 0; j < length - i - 1; j++) 
                {
                    createColor([j, j + 1], delay, 'COMPARE');
                    delay += 100;
                    if(tempArr[j] > tempArr[j + 1]) 
                    {
                        createAnimation(tempArr, j, j + 1, delay);
                        delay += 100;
                        swapped = true;
                    }
                    createColor([], delay, 'NONE');
                    delay += 100;
                }
                tempDoneElements.push(length - i - 1);
                createColor(tempDoneElements, delay, 'DONE');
                delay += 100;
    
                // If no two elements were  
                // swapped by inner loop, then break 
                if(swapped === false) break;
            }
    
            for(let k = 0; k < length - i - 1; k++) {
                tempDoneElements.push(k);
            }
            createColor(tempDoneElements, delay, 'DONE');
            delay += 100;
        }
    
        const createAnimation = (tempArr, indexOne, indexTwo, delay) => {
            const temp = tempArr[indexOne];
            tempArr[indexOne] = tempArr[indexTwo];
            tempArr[indexTwo] = temp;
            const newArr = [...tempArr];
            setTimeout(() => {
                setArr([...newArr]);
              }, delay);
        }
    
        const createColor = (tempElements, delay, action) => {
            switch(action) {
                case 'DONE': 
                    const newDoneElements = [...tempElements];
                    setTimeout(() => {
                        setDoneElements([...newDoneElements]);
                    }, delay);
                    break;
                case 'COMPARE':
                    setTimeout(() => {
                        setCompareElements([...tempElements]);
                    }, delay);
                    break;
                default:
                    setTimeout(() => {
                        setCompareElements([]);
                    }, delay);
            }
        }
    
        const maxVal = Math.max(...arr);
        
        return (
            <div>
                <div className="array-container" style={{height: '50%'}}>
                    {arr.map((value, idx) => (
                        <div className="array-element"
                             key={shortid.generate()}
                             style={{height: `${(value * 100 / maxVal).toFixed()}%`,
                                     width: `calc(${100 / length}% - 2px)`,
                                     margin: '0 1px',
                                     display: 'inline-block',
                                     backgroundColor: compareElements.includes(idx) ? 'darkred' : 
                                                      doneElements.includes(idx) ? 'green' : 'black',
                                     color: 'white'}}
                        ></div>))
                    }
                </div>
                <div>
                    <button onClick={() => generateArray()}>New array</button>
                    <button onClick={() => bubbleSort()}>Sort</button>
                </div>
                <div className="slider-container">
                    1
                    <input type="range" 
                           min="1" 
                           max="100" 
                           onChange={(e) => handleLength(e)} 
                           className="slider" 
                           id="myRange" 
                    />
                    100
                </div>
                {length}
            </div>
        );
    }
    
    export default Dummy2;
    

    Instead of messing with the DOM I used state to keep track of changes so react will be charge of changing things.

    When manipulating the array in the sorting function we need to remember that arr is part of state therefore it is immutable. any change that we do we need to do on a duplicate and apply the changes at the right time so an animation will occur, that is what I done in the createAnimation function.

    To keep track on the colors I added to the state doneElements and compareElements. Every time an element get to it's final position it's index is added to doneElements. At any given time only two elements are compared therefore compareElements will contain only two elements or none.