useEffect以数组为依赖时检测数组顺序变化的最佳方法
数组作为useEffect依赖时仅检测元素顺序变更的实现方案
现有使用JSON.stringify(columnOrder)作为依赖的写法存在明显缺陷:
- 数组长度变化、元素增删、元素值修改都会触发effect,完全不符合「忽略长度变化、仅响应顺序变更」的需求
- 大数组序列化存在不必要的性能开销,碰到undefined、Symbol、循环引用等特殊值时会出现序列化异常
- 属于React官方不推荐的依赖写法,会触发hooks依赖规则告警
核心判断逻辑
要实现仅响应顺序变更的触发规则,必须对数组的新旧状态做三层校验,只有全部满足才触发effect:
- 新旧数组长度完全一致
- 新旧数组包含的元素完全相同(每个元素的出现次数一致,排除增删、改值场景)
- 相同索引位置的元素存在差异(即排列顺序发生变化)
推荐实现:自定义比对逻辑
第一步:修复原有工具函数的隐式bug
原mapOrder函数直接调用Array.sort()会原地修改传入的原数组,破坏React状态的不可变性,必须先浅拷贝数组再执行排序:
export const mapOrder = <ObjectType, KeyType extends keyof ObjectType>({ array, order, key, }: { array: ObjectType[]; order: ObjectType[KeyType][]; key: KeyType; }) => { // 浅拷贝后再排序,避免修改原数组 return [...array].sort((a, b) => { const A = a[key], B = b[key]; return order.indexOf(A) - order.indexOf(B); }); };
第二步:通过useRef存储上一次状态,实现自定义触发判断
不需要额外封装复杂hook,直接在组件内通过ref缓存上一次的数组值,在effect中先做校验,不符合触发条件就直接终止执行:
// 缓存上一次的columnOrder值 const prevColumnOrderRef = useRef<string[]>([]); useEffect(() => { const prevOrder = prevColumnOrderRef.current; const currOrder = columnOrder; // 长度变化:不属于顺序变更,直接缓存新值后退出 if (prevOrder.length !== currOrder.length) { prevColumnOrderRef.current = [...currOrder]; return; } // 元素集合不一致(存在增删/改值):不属于顺序变更,缓存后退出 const prevElementSet = [...prevOrder].sort().join(','); const currElementSet = [...currOrder].sort().join(','); if (prevElementSet !== currElementSet) { prevColumnOrderRef.current = [...currOrder]; return; } // 顺序完全一致:不需要更新,直接退出 const isOrderSame = prevOrder.every((item, idx) => item === currOrder[idx]); if (isOrderSame) return; // 确认是顺序变更场景:缓存新值,执行列排序更新 prevColumnOrderRef.current = [...currOrder]; changeColumnsState(prevState => mapOrder({ array: prevState, order: currOrder, key: 'name' }) ); // 依赖仅保留columnOrder即可,不需要额外依赖 }, [columnOrder]);
注意:状态更新使用函数式写法
changeColumnsState(prevState => ...),可以避免闭包陷阱,不需要把columnsState加入依赖数组。
其他可选实现:生成顺序专属依赖值
如果不想把判断逻辑写在effect内部,可以封装通用hook,生成仅在顺序变更时更新的依赖值,用法和原来的JSON.stringify依赖完全一致:
const useOrderOnlyDep = <T>(arr: T[]) => { const prevArrRef = useRef<T[]>([]); const validSignRef = useRef<string>(''); const prev = prevArrRef.current; const curr = arr; // 长度变化、元素集合变化时复用上一次的签名,不触发更新 if (prev.length !== curr.length || [...prev].sort().join(',') !== [...curr].sort().join(',')) { prevArrRef.current = [...curr]; return validSignRef.current; } // 长度、元素集合一致时,返回当前顺序的签名 const currSign = curr.join(',|'); prevArrRef.current = [...curr]; validSignRef.current = currSign; return currSign; }; // 组件内使用 const columnOrderDep = useOrderOnlyDep(columnOrder); useEffect(() => { changeColumnsState(prevState => mapOrder({ array: prevState, order: columnOrder, key: 'name' }) ); }, [columnOrderDep]);
内容的提问来源于stack exchange,提问作者Daniel Sadovskiy
相关产品推荐
相关产品推荐

