1
\$\begingroup\$

I just started learning React for a university project to make an algorithms visualizer. I need a review of my code below for visualizing selection sort. I would also appreciate suggestion on how to make it more extensible : I want to add other sorts (bubble, insertion, quick and merge) as well, so nesting all of them in the SortingVisualizer component would make it look terrible and hard to manage.

import {useRef, useState} from 'react';


const normalBarColor = 'red';
const modifiedBarColor = 'green';
const containerColor = 'black';
const beingComparedColor = 'yellow';
const beingSwappedColor = 'white';
const settledColor = 'orange';

function ArrayBar(props) {
    const scalingFactor = 100;
    const value = props.value;
    const color = props.color;
    const style = {
        backgroundColor: color,
        height: `${value * scalingFactor}px`,
        width: '10px',
        margin: `10px`
    };
    return <div className='arrayBar' style={style}></div>
}


function ArrayContainer(props) {
    const arrayBars = props.arrayBars;
    const style = {
        backgroundColor: containerColor,
        display: "flex",
        flexDirection: "row",
        height: '800px',
        width: '800px',
    }
    return <div style={style}>
        {arrayBars.map((bar, i) => <ArrayBar key={i} value={bar.value} color={bar.color}/>)}
    </div>;
}

function SelectionSortButton(props) {
    const onClickHandler = props.onClick;
    return <div>
        <button onClick={onClickHandler}>Sort</button>
    </div>
}


export default function SortingVisualizer() {
    const dataInit = [7, 6, 5, 4, 3, 2, 1];
    const arrayBarsInit = [];
    const COMPARE = 1;
    const SWAP = 2;
    const DO_NOTHING = 3;
    for (let i = 0; i < dataInit.length; i++) {
        arrayBarsInit.push({value: dataInit[i], color: normalBarColor});
    }

    const [arrayBars, setArrayBars] = useState(arrayBarsInit);
    const iRef = useRef(0);
    const jRef = useRef(iRef.current + 1);
    const minIndexRef = useRef(0);
    const actionRef = useRef(COMPARE);

    function modifyArrayBars(newArrayBars) {
        const arrayBarsCopy = [...newArrayBars];
        setArrayBars(arrayBarsCopy);
    }

    function handleNextSelectionStepClick() {
        selectionSort(arrayBars);
    }

    async function selectionSort(arrayBars) {
        const newArrayBars = [...arrayBars];
        const N = arrayBars.length;

        // compare or swap?
        if (actionRef.current === COMPARE) {
            console.log("comparing ", minIndexRef.current, jRef.current);
            const initialMinIndex = minIndexRef.current;
            const initialJ = jRef.current;
            newArrayBars[jRef.current].color = beingComparedColor;
            newArrayBars[minIndexRef.current].color = beingComparedColor;

            modifyArrayBars(newArrayBars);

            if (newArrayBars[jRef.current].value < newArrayBars[minIndexRef.current].value) {
                minIndexRef.current = jRef.current;
            }

            await new Promise(r => setTimeout(r, 200));

            jRef.current += 1;
            if (jRef.current === N) {
                actionRef.current = SWAP;
            }

            newArrayBars[initialJ].color = normalBarColor;
            newArrayBars[initialMinIndex].color = normalBarColor;
            modifyArrayBars(newArrayBars);
        } else if (actionRef.current === SWAP) {
            newArrayBars[minIndexRef.current].color = beingSwappedColor;
            newArrayBars[iRef.current].color = beingSwappedColor;

            modifyArrayBars(newArrayBars);

            await new Promise(r => setTimeout(r, 200));

            const temp = newArrayBars[iRef.current];
            newArrayBars[iRef.current] = newArrayBars[minIndexRef.current];
            newArrayBars[minIndexRef.current] = temp;

            newArrayBars[minIndexRef.current].color = normalBarColor;
            newArrayBars[iRef.current].color = settledColor;
            modifyArrayBars(newArrayBars);

            iRef.current += 1;
            minIndexRef.current = iRef.current;
            console.log('iRef.current = ', iRef.current);
            jRef.current = iRef.current < N - 1 ? iRef.current + 1 : iRef.current;
            if (iRef.current === N) {
                actionRef.current = DO_NOTHING;
            } else {
                actionRef.current = COMPARE;
            }
        } else if (actionRef.current === DO_NOTHING) {

        }
    }

    return (
        <div>
            <ArrayContainer arrayBars={arrayBars} modifyArrayBars={modifyArrayBars}></ArrayContainer>
            <SelectionSortButton onClick={handleNextSelectionStepClick}></SelectionSortButton>
        </div>
    );
}
```
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Your selectionSort does two things: sort + display. Consider defining an API (or data structure) that lets each sort algorithm report its actions, like compare / swap indexes i & j, rather than display its actions. Then you can have a sorter and a new display routine that each do just one thing. And the quicksort you add in future will be smaller and easier to write. Reports could go to a basic display, and perhaps to a fancier UI that lets you scroll forward & backward in time. Or show stacked display of two reports. \$\endgroup\$
    – J_H
    Commented Mar 19, 2023 at 15:37

1 Answer 1

2
\$\begingroup\$

Things I love about the code:

  • Your components are small.
  • Your use of design tokens. (normalBarColor, modifiedBarColor, containerColor…)
  • You have a clear separation between your js and jsx.

Issues found:

function ArrayContainer(props) {
    ...
    {arrayBars.map((bar, i) => <ArrayBar key={i} value={bar.value} color={bar.color}/>)}
    ...
}

Avoid using index (i) as a key. See: why-does-react-need-keys. tldr; better performance

function SelectionSortButton(props) {
    const onClickHandler = props.onClick;
    return <div>
        <button onClick={onClickHandler}>Sort</button>
    </div>
}

This component doesn’t do anything, consider removing

export default function SortingVisualizer() {
...
        // prefer dataInit.map over for ()
        for (let i = 0; i < dataInit.length; i++) {
        // prefer immutable (avoid push) 
        arrayBarsInit.push({value: dataInit[i], color: normalBarColor});
    }
...
                                // this seems to block the render process
                                // Never block the render process.  
                await new Promise(r => setTimeout(r, 200));
...
    // ArrayContainer ignores modifyArrayBars
   <ArrayContainer arrayBars={arrayBars} modifyArrayBars={modifyArrayBars}></ArrayContainer>

I was unable to follow your logic in SortingVisualizer. I’ve documented some code smells. You are doing too much in handleNextSelectionStepClick > selectionSort. This is a sign you have not found your minimal state. (See: Find minimal state)

My Versions

Here is my version of Visual sort. I hope you can update it to meet your needs:

https://codesandbox.io/s/sorting-visualizer-in-react-r86mos

import { useState } from "react";

const activeBarColor = "yellow";
const normalBarColor = "red";
const modifiedBarColor = "green";
const containerColor = "black";

// Anything that can be moved out of functions should be moved outside a functions
const SCALING_FACTOR = 100;

// I've modified ArrayBar to take one property "bar", bar holds data about the bar (no longer
// color) and ArrayBar renders based on that data.   Separating values and presentation is a
// good practice.
function ArrayBar(props) {
  const { value, isSorted, isLast } = props.bar;
  const sortedColor = isLast ? activeBarColor : modifiedBarColor;
  const color = isSorted ? sortedColor : normalBarColor;
  const style = {
    backgroundColor: color,
    height: `${value * SCALING_FACTOR}px`,
    width: "10px",
    margin: `10px`,
  };
  return <div className="arrayBar" style={style}></div>;
}

function ArrayContainer(props) {
  const arrayBars = props.arrayBars;
  const style = {
    backgroundColor: containerColor,
    display: "flex",
    flexDirection: "row",
    height: "800px",
    width: "800px",
  };
  // notice we are using bar.value for the key.  We can do that as long
  // as values are unique.
  return (
    <div style={style}>
      {arrayBars.map((bar) => (
        <ArrayBar key={bar.value} bar={bar} />
      ))}
    </div>
  );
}

// I've added some unique values to make the sort more interesting
const DATA_INIT = [7, 6, 5, 6.6, 2.1, 4, 3, 2, 1];
const sortByValue = (a, b) => (a.value > b.value ? 1 : -1);

export default function SortingVisualizer() {
  const [step, setStep] = useState(0);
  const handleNextSelectionStepClick = () => {
    setStep((oldIndex) => oldIndex + 1);
  };
  const sortedArray = DATA_INIT.filter((v, i) => i <= step) // filter out unsorted
    .map((value, i) => ({ value, isSorted: true, isLast: i === step })) // transform value to bar object
    .sort(sortByValue);

  const unsortedArray = DATA_INIT.filter((v, i) => i > step) // filter out sorted
    .map((value) => ({ value })); // transform value to bar object

  const arrayBars = [...sortedArray, ...unsortedArray];  // concatenate the two array

  return (
    <div>
      <ArrayContainer arrayBars={arrayBars}></ArrayContainer>
      <button onClick={handleNextSelectionStepClick}>Sort</button>
    </div>
  );
}

Second iteration using styled components and a second sort. I wish I could help more with showing more real sort algorithms, but it's been 30 years since I took that course.
https://codesandbox.io/s/sorting-visualizer-in-react-2-t2wnht

import { useState } from "react";
import styled, { css } from "styled-components";
const activeBarColor = "yellow";
const normalBarColor = "red";
const modifiedBarColor = "green";
const containerColor = "black";
const nextColor = "lime";

// Anything that can be moved out of functions should be moved outside a 
// functions
const SCALING_FACTOR = 50;

const ArrayBar = styled.div((props) => {
  const { value, isSorted, isLast, isNext } = props.bar;
  const sortedColor = isLast ? activeBarColor : modifiedBarColor;
  const unsortedColor = isNext ? nextColor : normalBarColor;
  const color = isSorted ? sortedColor : unsortedColor;

  return css`
    background-color: ${color};
    height: ${value * SCALING_FACTOR}px;
    width: 10px;
    margin: 10px;
  `;
});

const Container = styled.div`
  background-color: ${containerColor};
  display: flex;
  flex-direction: row;
  height: 400px;
  width: 800px;
`;

function ArrayContainer(props) {
  const arrayBars = props.arrayBars;
  // notice we are using bar.value for the key.  We can do that as long
  // as values are unique.
  return (
    <Container>
      {arrayBars.map((bar) => (
        <ArrayBar key={bar.value} bar={bar} />
      ))}
    </Container>
  );
}

// I've added some unique values to make the sort more interesting
const DATA_INIT = [7, 6, 5, 6.6, 2.1, 4, 3, 2, 1];
const sortByValue = (a, b) => (a.value > b.value ? 1 : -1);
const initSorted = [...DATA_INIT].sort();

const otherSort = (step) => {
  const sortedArray = initSorted.filter((v, i) => i <= step); // filter out unsorted
  const sortedOjbectArray = sortedArray.map((value, i) => ({
    value,
    isSorted: true,
    isLast: i === step
  })); // transform value to bar object

  const unsortedArray = DATA_INIT.filter(
    (value) => !sortedArray.includes(value)
  ) // filter out sorted
    .map((value) => ({ value, isNext: initSorted[step + 1] === value })); // transform value to bar object

  console.log({ unsortedArray });

  return [...sortedOjbectArray, ...unsortedArray]; // concatenate the two array
};

const insertionLikeSort = (step) => {
  const sortedArray = DATA_INIT.filter((v, i) => i <= step) // filter out unsorted
    .map((value, i) => ({ value, isSorted: true, isLast: i === step })) // transform value to bar object
    .sort(sortByValue);

  const unsortedArray = DATA_INIT.filter((v, i) => i > step) // filter out sorted
    .map((value, i) => ({ value, isNext: i === 0 })); // transform value to bar object

  return [...sortedArray, ...unsortedArray]; // concatenate the two array
};

export default function SortingVisualizer() {
  const [step, setStep] = useState(0);
  const handleNextSelectionStepClick = () => {
    setStep((oldIndex) => oldIndex + 1);
  };
  const otherBars = otherSort(step);
  const insertBars = insertionLikeSort(step);

  return (
    <div>
      <ArrayContainer arrayBars={insertBars}></ArrayContainer>
      <button onClick={handleNextSelectionStepClick}>Sort</button>
      <ArrayContainer arrayBars={otherBars}></ArrayContainer>
    </div>
  );
}

Other thoughts:

  • This may be obvious, but put each component into a separate file.  (You probably knew this).
  • Be sure to use a linter. It can catch many common issues.
  • Look into styled-components
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.