1

I am learning react by working on a sorting algorithm visualizer and I want to update the state array that is rendered, regularly in a loop.

Currently I am passed an array with pairs of values, first indicating the current index and value, and second with its sorted index and value.

[(firstIdx, value), (sortedIdx, value), (secondIdx, value), (sortedIdx, value) ... etc]

some actual values:

`[[1, 133], [0, 133], [2, 441], [2, 441], [3, 13], [0, 13] ... ]`

What I want to do is cut the value out of the array, splice it into the correct position, while updating the state array rendered in each step. effectively creating an animation with the state array.

Right now when I run below function, my state array instantly becomes the sorted array because of the batching. I would like there to be a delay between each state update in the loop.

code snippet that I've tried.

    insertionSort(changeArray) {
        const arrayBars = document.getElementsByClassName('array-bar')
        // I want to keep track which index to move from/to so I instantiate it outside the loop.

        let [barOneIdx, barOneValue] = [0, 0];
        let [barTwoIdx, barTwoValue] = [0, 0];

        // Copy of the state array that I will modify before setting the state array to this.
        let auxArray = this.state.array.slice();
        for (let i = 0; i < changeArray.length; i++) {
            // This tells me whether it is the first or second pair of values.
            let isFirstPair = 1 % 2 !== 1;

            if (isFirstPair) {
                // first set of values is the current index + height
                [barOneIdx, barOneValue] = changeArray[i];

                // Changes the current bar to green.
                setTimeout(() => {
                    arrayBars[barOneIdx].style.backgroundColor = 'green';
                }, i * 300);

            } else {
                // second set of values is the sorted index + height.
                [barTwoIdx, barTowValue] = changeArray[i];

                // Cut the current bar out of the array.
                let cutIdx = auxArray[barOneIdx];
                auxArray.splice(barOneIdx, 1);

                // Splice it into the sorted index
                auxArray.splice(barTwoIdx, 0, cutIdx);

                // Changes the color of the bar at the correct sorted 
                // index once, and then again to revert the color.
                setTimeout(() => {

                    // Set the state array with the new array.    NOT WORKING
                    // Instantly sets state array to final sorted array.
                    // I want this to run here with a delay between each loop iteration.
                    this.setState({ array: auxArray });

                    arrayBars[barTwoIdx].style.backgroundColor = SECONDARY_COLOR;
                }, i * 300);
                setTimeout(() => {
                    arrayBars[barTwoIdx].style.backgroundColor = PRIMARY_COLOR;
                }, i * 300);
            }
        }
    }

https://codesandbox.io/s/eager-yonath-xpgjl?file=/src/SortingVisualizer/SortingVisualizer.jsx

link to my project so far with all the relevant functions and files.

On other threads say not to use setState in a loop as they will be batched and run at the end of the block code. Their solutions won't work for my project though as I want to create an animation with the state array.

What would be the best way to implement this?

Sujio
  • 357
  • 1
  • 2
  • 13
  • Have you tried using promises? – Yves Gonzaga Jan 03 '21 at 03:20
  • I've brushed over promises in school but not sure how I would apply it here, could you show a quick example? – Sujio Jan 03 '21 at 03:33
  • Post more of your code. Show the `getInsertionSortAnimations` function. What are `SECONDARY_COLOR, ANIMATION_SPEED_MS, PRIMARY_COLOR` set to? In order to help you, we need to be able to reproduce what you're doing and getting snippets of code is not how get there. – codemonkey Jan 03 '21 at 04:15
  • edited with requested code, I thought it wasn't important for what I'm trying to do but its there now. – Sujio Jan 03 '21 at 04:23
  • `randomIntfromInterval` function is missing. You should load your code up into codesandbox and get it to work as it's currently working. Then describe your desired behavior and I am pretty positive this community will help figure out your problem. It's just much easier to follow when you illustrate the issue. Here I have even started a sandbox for you, just fork it: https://codesandbox.io/s/holy-haze-qz9sw?file=/src/App.js – codemonkey Jan 03 '21 at 04:58
  • https://codesandbox.io/s/eager-yonath-xpgjl?file=/src/App.js Wow I didn't know a site like this existed! I pretty much uploaded my whole project as is and its working the same way as my browser right now. I've also edited my original post with how I imagine the state array would work. Thank you a lot! – Sujio Jan 03 '21 at 05:11
  • Great job on Sandboxing it! It may just be too late in the day or, possibly, I simply don't speak English well enough, but I'm having serious trouble following your "What I want to happen" section. So here is my question. When I click the "Insertion Sort" button what should happen to those 5 random bars? – codemonkey Jan 03 '21 at 07:23
  • Thanks for taking a look.It should highlight one bar, delay, put into sorted position highlighted, delay, highlight next bar, delay, put into sorted position, delay. and repeat until the sorted array is fully animated. Currently, it instantly changes to the sorted array. – Sujio Jan 03 '21 at 07:29
  • Got it. You want to visualize the sort. And you want to do that at the speed of `ANIMATION_SPEED_MS`. Currently your sort happens immediately. I think I got it now. – codemonkey Jan 03 '21 at 07:31
  • Tricky indeed. Will take a look at it tomorrow morning when my brain isn't fried. – codemonkey Jan 03 '21 at 07:36
  • I know that feel, I've also completely re-edited the post to be clearer about what I am asking without the extra stuff. – Sujio Jan 03 '21 at 07:41

2 Answers2

1

If you are familiar with ES6 async/await you can use this function

async function sleep(millis) {
  return new Promise((resolve) => setTimeout(resolve, millis));
}

async function insertionSort() {
   // you code logic
   await sleep(5000) //delay for 5s
   this.setState({array : auxArray});

}
mirsahib
  • 375
  • 1
  • 4
  • 12
0

I have put together a basic version of what you're trying to achieve. I depart from your approach in a few important ways, one of which is the use of an async function to delay state updates. I also change the way the original array is generated. I now create an array that includes the height of the bars as well as its color. This is necessary to accomplish the color changes while moving the bars to their right spots.

There is also no need for your getInsertionSortAnimations function any more as the sorting is done inside the class using the reduce function. I will just paste the entire code here for future reference, but here is the Sandbox link: https://codesandbox.io/s/damp-bush-gkq2q?file=/src/SortingVisualizer/SortingVisualizer.jsx

import React from "react";
import "./SortingVisualizer.css";
//import { getMergeSortAnimations } from "../SortingAlgorithms/MergeSort";
import { setTimeout } from "timers";

// Original color of the array bars.
const PRIMARY_COLOR = "aqua";

// Color we change to when we are comparing array bars.
const SECONDARY_COLOR = "green";

// Speed of the animation in ms.
const ANIMATION_SPEED_MS = 400;

// Number of array bars.
const NUMBER_OF_BARS = 10;

const sleep = (millis) => {
  return new Promise((resolve) => setTimeout(resolve, millis));
};

function arraymove(arr, fromIndex, toIndex) {
  var element = arr[fromIndex];
  arr.splice(fromIndex, 1);
  arr.splice(toIndex, 0, element);
}

export default class SortingVisualizer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      array: []
    };
  }

  // React function runs first time component is rendered, client side only.
  componentDidMount() {
    this.resetArray();
  }

  resetArray() {
    const array = [];
    for (let i = 0; i < NUMBER_OF_BARS; i++) {
      array.push({
        height: randomIntfromInterval(10, 200),
        color: PRIMARY_COLOR
      });
    }
    this.setState({ array });
  }

  animateSorting = async (sorted_array) => {
    const { array } = this.state;
    for (let i = 0; i < sorted_array.length; i++) {
      const orig_index = array.findIndex(
        (item) => item.height === sorted_array[i]
      );
      array[orig_index].color = SECONDARY_COLOR;
      this.setState(array);
      await sleep(ANIMATION_SPEED_MS);
      arraymove(array, orig_index, i);
      this.setState(array);

      if (orig_index !== i) await sleep(ANIMATION_SPEED_MS);
    }
  };

  insertionSort() {
    const { array } = this.state;
    const sorted = array.reduce((sorted, el) => {
      let index = 0;
      while (index < sorted.length && el.height < sorted[index]) index++;
      sorted.splice(index, 0, el.height);
      return sorted;
    }, []);

    this.animateSorting(sorted);
  }

  render() {
    const { array } = this.state;

    return (
      // Arrow function to use "this" context in the resetArray callback function: this.setState({array}).
      // React.Fragment allows us to return multiple elements under the same DOM.
      <React.Fragment>
        <div className="button-bar">
          <button onClick={() => this.resetArray()}>Generate Array</button>
          <button onClick={() => this.insertionSort()}>Insertion Sort</button>
          <button onClick={() => this.mergeSort()}>Merge Sort</button>
          <button onClick={() => this.quickSort()}>Quick Sort</button>
          <button onClick={() => this.heapSort()}>Heap Sort</button>
          <button onClick={() => this.bubbleSort()}>Bubble Sort</button>
        </div>
        <div className="array-container">
          {array.map((item, idx) => (
            <div
              className="array-bar"
              key={idx}
              // $ dollarsign makes a css variable???
              style={{
                backgroundColor: `${item.color}`,
                height: `${item.height}px`
              }}
            ></div>
          ))}
        </div>
      </React.Fragment>
    );
  }
}

// Generates random Integer in given interval.
// From https://stackoverflow.com/questions/4959975/generate-random-number-between-two-numbers-in-javascript
function randomIntfromInterval(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
}

Gotcha: There is a bug in the code however, which should be a piece of cake for you to fix. Notice how it behaves when two or more bars happen to have the same exact height.

codemonkey
  • 7,325
  • 5
  • 22
  • 36
  • This is great! Exactly what I was hoping to achieve. I see now how I was using promises wrong and is much easier than trying to change the color with css. Thank you for your help. – Sujio Jan 04 '21 at 00:51
  • @Sujio don't forget about that homework having to do with identical bar heights. – codemonkey Jan 04 '21 at 01:06
  • Yup, I've added an extra check before I change the colors. I up voted you but I'm too new for it to count. I salute you :) – Sujio Jan 04 '21 at 01:26