ReactJS useReducer触发两个不同Action问题排查与解决
排序可视化工具的Action调度问题及修复方案
我正在构建一个排序算法可视化工具,点击按钮后每秒对整数数组中的一个元素进行排序。但点击“selection sort”按钮时,会先调度“randomize”类型的action,再调度“selection sort”的action,且该按钮实际仅起到“randomize”按钮的作用,无法完成排序。请问我是否错误使用了useReducer Hook?如何让它仅调度一个action?
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;
问题分析与修复
1. 核心问题:Switch语句缺少Break
你的ControlPanel组件中,useEffect里的switch语句未添加break,导致当algorithm为"selection sort"时,执行完选择排序的dispatch后,会继续执行下一个case的随机化代码,同时调度两个action。这就是点击选择排序按钮却触发随机化的直接原因。
修复代码:
useEffect(() => { switch(algorithm) { case "selection sort": dispatch({ type: "selection sort" }); setAlgorithm(""); break; case "randomize": dispatch({ type: "randomize" }); setAlgorithm(""); break; default: break; } }, [algorithm]);
2. Reducer异步逻辑错误
React的useReducer要求reducer是纯函数,不能包含异步操作(如setTimeout)。你当前在reducer中用定时器执行排序逻辑,会导致:
- Reducer会立即执行完循环,不会等待定时器回调
- 回调中的
return无法被reducer捕获,状态根本不会更新
正确方案:将异步调度移至组件,分步骤触发排序
步骤1:扩展Action类型
添加支持单步排序的action,传递当前处理索引:
export type Action = | { type: "selection_sort_step", index: number } | { type: "randomize" }
步骤2:修改Reducer处理单步排序
让reducer仅处理当前步骤的排序,返回新数组:
const arrReducer = (arr: number[], action: Action) => { switch (action.type) { case "selection_sort_step": { const i = action.index; const newArr = [...arr]; let minIndex = i; for (let j = i + 1; j < newArr.length; j++) { if (newArr[j] < newArr[minIndex]) { minIndex = j; } } if (minIndex !== i) { [newArr[i], newArr[minIndex]] = [newArr[minIndex], newArr[i]]; } return newArr; } case "randomize": { const newArr: number[] = []; while (newArr.length < 10) { let rng = Math.floor(Math.random() * 10) + 1; if (newArr.indexOf(rng) === -1) { newArr.push(rng); } } return newArr; } default: return arr; } }
步骤3:在组件中调度单步排序
修改ControlPanel的useEffect,通过定时器依次触发每一步:
useEffect(() => { switch(algorithm) { case "selection sort": for (let i = 0; i < arr.length; i++) { setTimeout(() => { dispatch({ type: "selection_sort_step", index: i }); }, 1000 * (i + 1)); // 每秒执行一步 } setAlgorithm(""); break; case "randomize": dispatch({ type: "randomize" }); setAlgorithm(""); break; default: break; } }, [algorithm, arr.length, dispatch]);
3. 其他注意事项
- 永远不要直接修改reducer中的原数组,必须返回新数组(如
const newArr = [...arr]),否则React无法检测状态变化 - 原代码中
defaultcase的[...arr.slice()]无意义,直接返回原数组即可
内容的提问来源于stack exchange,提问作者bascott
相关产品推荐
相关产品推荐

