React函数组件setColumns更新成功后onColumnClick无法读取状态且存在循环依赖
问题根因
你遇到的是典型的React闭包陷阱问题:
- 你用
useCallback包裹onColumnClick时传入了空依赖数组,这会让这个函数永远捕获组件首次渲染时的状态值,首次渲染时columns的初始值就是[],pods初始值是undefined,所以后续无论状态怎么更新,这个函数里读到的columns永远是空数组。点击列头时,空数组做slice得到的还是空数组,调用setColumns把列配置直接改成了空,列表没有列可以渲染,看起来就像消失了。 - 你初始化
columns的useEffect也用了空依赖数组,只会在组件首次挂载时执行一次,执行时绑定到列配置上的onColumnClick就是首次渲染的旧版本,就算后续函数更新,列配置绑定的还是旧函数。
你感知到的循环依赖问题确实存在:如果直接把columns、pods加到onColumnClick的依赖数组里,onColumnClick会每次状态更新都重新生成,如果你还在useEffect里依赖onColumnClick初始化列,就会反复触发useEffect执行,陷入死循环。
解决方法
方案1(最推荐):把静态列配置移出副作用,从根源消除循环依赖
你的列配置本身是静态的,完全不需要放在useEffect里初始化,直接作为useState的初始值即可:
import React, { useState, useEffect, useCallback } from "react"; import { DetailsListLayoutMode, SelectionMode, IColumn } from "@fluentui/react/lib/DetailsList"; import { ShimmeredDetailsList } from "@fluentui/react/lib/ShimmeredDetailsList"; export interface IPod { name: string; status: string; age: number; } // 静态列配置定义在组件外,避免每次渲染重复生成,先不绑定点击事件 const initColumns: IColumn[] = [ { key: "column1", name: "File Type", ariaLabel: "Column operations for File type, Press to sort on File type", iconName: "Page", isIconOnly: true, fieldName: "na2me", minWidth: 16, maxWidth: 16, }, { key: "pod-name", name: "Pod Name", fieldName: "name", minWidth: 210, maxWidth: 350, isRowHeader: true, isResizable: true, isSorted: true, isSortedDescending: false, sortAscendingAriaLabel: "Sorted A to Z", sortDescendingAriaLabel: "Sorted Z to A", data: "string", isPadded: true }, { key: "pod-status", name: "Status", fieldName: "status", minWidth: 70, maxWidth: 90, isResizable: true, data: "string", isPadded: true }, { key: "pod-age", name: "Age", fieldName: "timestamp", minWidth: 70, maxWidth: 90, isResizable: true, isCollapsible: true, data: "number", onRender: (pod: IPod) => { var age = ""; if (pod.age > 86400) age = Math.floor(pod.age / 86400).toString() + " day"; else if (pod.age > 3600) age = Math.floor(pod.age / 3600).toString() + " hour"; else if (pod.age > 60) age = Math.floor(pod.age / 60).toString() + "min"; else age = pod.age.toString() + " sec"; return ( <span style={{ display: "block", textAlign: "right" }}>{age}</span> ); }, isPadded: true }, { key: "column5", name: "File Size", fieldName: "fileSizeRaw", minWidth: 70, maxWidth: 90, isResizable: true, isCollapsible: true, data: "number", } ] const PodList: React.FunctionComponent = () => { const [pods, setPods] = useState<IPod[] | undefined>(undefined); // 直接用静态配置初始化columns const [columns, setColumns] = useState<IColumn[]>(initColumns); type FunctionType<T> = ( items: T[], columnKey: string, isSortedDescending?: boolean ) => T[]; const onColumnClick = useCallback( (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => { const copyAndSort: FunctionType<IPod> = ( items, columnKey, isSortedDescending ) => { const key = columnKey as keyof IPod; return items .slice(0) .sort((a: IPod, b: IPod) => (isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1 ); }; // 用setColumns函数式更新直接拿最新值,不需要依赖外部状态 setColumns(prevColumns => { const newColumns: IColumn[] = prevColumns.slice(); const currColumn: IColumn = newColumns.filter( (currCol) => column.key === currCol.key )[0]; newColumns.forEach((newCol: IColumn) => { if (newCol === currColumn) { currColumn.isSortedDescending = !currColumn.isSortedDescending; currColumn.isSorted = true; } else { newCol.isSorted = false; newCol.isSortedDescending = true; } }); return newColumns }); // pods也用函数式更新拿最新值 setPods(prevPods => { if(!prevPods) return prevPods return copyAndSort(prevPods, column.fieldName!, !column.isSortedDescending) }) }, [] // 无需任何依赖,不会频繁生成新函数 ); // 统一给所有列绑定点击事件 const processedColumns = columns.map(col => ({ ...col, onColumnClick })) useEffect(() => { // 副作用只保留数据请求逻辑 const url = "data.json"; const fetchData = async () => { try { const response = await fetch(url, { mode: "cors" }); const json = await response.json(); if (Array.isArray(json)) { setPods(json); } else { console.log(json); } } catch (error) { console.log("error", error); } }; fetchData(); }, []); const onItemInvoked = useCallback((pod?: IPod): void => {}, []); return ( <> <ShimmeredDetailsList items={pods || []} compact={false} columns={processedColumns} enableShimmer={!pods} selectionMode={SelectionMode.none} setKey="pods" layoutMode={DetailsListLayoutMode.justified} onItemInvoked={onItemInvoked} /> </> ); }; export default PodList;
这个方案彻底解决了循环依赖:onColumnClick不需要依赖外部状态,useEffect也不需要依赖onColumnClick,不会有反复执行的问题。
方案2:用useRef存储最新状态
如果你不想改动现有结构,可以用ref同步最新的columns和pods值,onColumnClick里直接读ref的值即可:
const PodList: React.FunctionComponent = () => { const [pods, setPods] = useState<IPod[] | undefined>(undefined); const [columns, setColumns] = useState<IColumn[]>([]); // 新建ref同步最新状态 const columnsRef = useRef(columns) const podsRef = useRef(pods) // 每次状态更新同步到ref useEffect(() => { columnsRef.current = columns podsRef.current = pods }, [columns, pods]) const onColumnClick = useCallback( (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => { // 从ref读取最新值,替代直接读状态 const currentColumns = columnsRef.current const currentPods = podsRef.current // 剩余逻辑和原有逻辑一致,把所有用到columns和pods的地方替换成上面两个变量即可 // ... }, [] ); // 其余原有代码保持不变 }
内容的提问来源于stack exchange,提问作者Mr.Wang from Next Door
相关产品推荐
相关产品推荐

