Search code examples
reactjsreact-hooksreact-typescript

ReactJS: useReducer dispatches two different actions rather than one


I am trying to build a sorting algorithm visualizer that sorts an element of an array containing integers once every second upon clicking a button. Whenever I click the "selection sort" button, however, it dispatches the "randomize" action type first, then "selection sort." This would be fine if it still sorted the array, but it doesn't, instead the "selection sort" button functionally acts as a second "randomize" button. Am I using the useReducer hook wrong? How can I make it so that only one action type is dispatched?

arr-context-provider.tsx

import { createContext, useReducer } from "react";
import ControlPanel from './components/control-panel';
import Visualizer from './components/visualizer';

interface ArrayContextProps {
  children: React.ReactElement;
}

export type Action = 
  | { type: "selection sort" }
  | { type: "randomize" }

const initArr: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export const arrContext = createContext<[number[], React.Dispatch<Action>]>([initArr, () => initArr]);

const ArrContextProvider: React.FC<ArrayContextProps> = ({ children }: ArrayContextProps) => {
  const arrReducer = (arr: number[], action: Action) => {
    switch (action.type) {
        case "selection sort":
          for (let i = 0; i < arr.length; i++) {
            setTimeout(() => {
              let minIndex = i;
              for (let j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                  minIndex = j;
                }
              }
              if (minIndex !== i) {
                [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
              }
              
              console.log("selection sort!");
              return [...arr.slice()];
            }, 100 * (i + 1));
          }
        
        case "randomize":
          arr = [];
          while (arr.length < 10) {
            let rng = Math.floor(Math.random() * 10) + 1;
            if (arr.indexOf(rng) === -1) {
                arr.push(rng);
            }
          }

          console.log("randomize!");
          return [...arr.slice()];
        
        default:
          console.log("error!");
          return [...arr.slice()];
    }
  }

  const [arr, dispatch] = useReducer<(arr: number[], action: Action) => number[]>(arrReducer, initArr);
  return (
    <arrContext.Provider value={[arr, dispatch]}>
      <div>
        <ControlPanel />
        <Visualizer />
      </div>
    </arrContext.Provider>
  );
};

export default ArrContextProvider;

control-panel.tsx

import { useState, useEffect, useContext } from 'react';
import { arrContext, Action } from './arr-context-provider';

const ControlPanel: React.FC = () => {
  const [arr, dispatch] = useContext<[number[], React.Dispatch<Action>]>(arrContext);
  const [algorithm, setAlgorithm] = useState("");

  useEffect(() => {
    switch(algorithm) {
      case "selection sort":
        dispatch({ type: "selection sort" });
      case "randomize":
        dispatch({ type: "randomize" });
        setAlgorithm("");
      default:
        [...arr.slice()];
    }
  }, [algorithm]);

  return (
    <div className='headerContainer'>
      <h1 className='header'>πŸ†‚πŸ…ΎπŸ†πŸ†ƒπŸ…ΈπŸ…½πŸ…Ά πŸ…°πŸ…»πŸ…ΆπŸ…ΎπŸ†πŸ…ΈπŸ†ƒπŸ…·πŸ…Ό πŸ†…πŸ…ΈπŸ†‚πŸ†„πŸ…°πŸ…»πŸ…ΈπŸ†‰πŸ…΄πŸ†</h1>
      <li className='list'>
        <ul><button className='button' onClick={() => setAlgorithm("selection sort")}>Selection Sort</button></ul>
        <ul><button className='button' onClick={() => setAlgorithm("randomize")}>Randomize</button></ul>
      </li>
    </div>
  )
}
  
export default ControlPanel;

Solution

  • I'm not sure you're using useContext properly here:

      const [arr, dispatch] = useContext<[number[], React.Dispatch<Action>]>(arrContext);

    The dispatch function should be coming from useReducer not useContext.

    From the documentation:

    import { useContext } from 'react';
    
    function MyComponent() {
      const theme = useContext(ThemeContext);
      // ...

    import { useReducer } from 'react';
    
    function reducer(state, action) {
      // ...
    }
    
    function MyComponent() {
      const [state, dispatch] = useReducer(reducer, { age: 42 });
      // ...