如果您正在使用 React,有一个非常常见的陷阱可能导致无限渲染。如果您未能为您的 columns、data 或 state 提供稳定的引用,React 将在表格状态发生任何更改时进入无限的重新渲染循环。
为什么会发生这种情况?这是 TanStack Table 中的错误吗?不,不是。这从根本上说是 React 的工作方式,正确管理您的列、数据和状态将防止这种情况发生。
TanStack Table 被设计为在传递到表格的 data 或 columns 发生更改时,或者在表格的任何状态发生更改时触发重新渲染。
未能为 columns 或 data 提供稳定引用可能导致无限的重新渲染循环。
export default function MyComponent() {
//😵 错误:这将导致无限的重新渲染循环,因为 `columns` 在每次渲染时都被重新定义为一个新数组!
const columns = [
// ...
];
//😵 错误:这将导致无限的重新渲染循环,因为 `data` 在每次渲染时都被重新定义为一个新数组!
const data = [
// ...
];
//❌ 列和数据在与 `useReactTable` 相同的范围内定义,没有稳定引用,将导致无限循环!
const table = useReactTable({
columns,
data,
});
return <table>...</table>;
}
export default function MyComponent() {
//😵 错误:这将导致无限的重新渲染循环,因为 `columns` 在每次渲染时都被重新定义为一个新数组!
const columns = [
// ...
];
//😵 错误:这将导致无限的重新渲染循环,因为 `data` 在每次渲染时都被重新定义为一个新数组!
const data = [
// ...
];
//❌ 列和数据在与 `useReactTable` 相同的范围内定义,没有稳定引用,将导致无限循环!
const table = useReactTable({
columns,
data,
});
return <table>...</table>;
}
在 React 中,您可以通过在组件外部/上方定义变量,或使用 useMemo 或 useState,或使用第三方状态管理库(如 Redux 或 React Query 😉)来为变量提供“稳定”引用。
//✅ 正确:在组件外部定义列
const columns = [
// ...
];
//✅ 正确:在组件外部定义数据
const data = [
// ...
];
// 通常在组件内部定义列和数据更实用,因此使用 `useMemo` 或 `useState` 为它们提供稳定引用
export default function MyComponent() {
//✅ 正确:这不会导致无限的重新渲染循环,因为 `columns` 是一个稳定引用
const columns = useMemo(() => [
// ...
], []);
//✅ 正确:这不会导致无限的重新渲染循环,因为 `data` 是一个稳定引用
const [data, setData] = useState(() => [
// ...
]);
// 列和数据在稳定引用中定义,不会导致无限循环!
const table = useReactTable({
columns,
data,
});
return <table>...</table>;
}
//✅ 正确:在组件外部定义列
const columns = [
// ...
];
//✅ 正确:在组件外部定义数据
const data = [
// ...
];
// 通常在组件内部定义列和数据更实用,因此使用 `useMemo` 或 `useState` 为它们提供稳定引用
export default function MyComponent() {
//✅ 正确:这不会导致无限的重新渲染循环,因为 `columns` 是一个稳定引用
const columns = useMemo(() => [
// ...
], []);
//✅ 正确:这不会导致无限的重新渲染循环,因为 `data` 是一个稳定引用
const [data, setData] = useState(() => [
// ...
]);
// 列和数据在稳定引用中定义,不会导致无限循环!
const table = useReactTable({
columns,
data,
});
return <table>...</table>;
}
即使您为初始 columns 和 data 提供了稳定引用,如果您就地修改它们,仍然可能遇到无限循环。这是一个常见的陷阱,您一开始可能没有注意到您正在这样做。如果您不小心,像内联 data.filter() 这样简单的操作都可能导致无限循环。
export default function MyComponent() {
//✅ 正确
const columns = useMemo(() => [
// ...
], []);
//✅ 正确(React Query 自动为数据提供稳定���用)
const { data, isLoading } = useQuery({
//...
});
const table = useReactTable({
columns,
//❌ 错误:这将导致无限的重新渲染循环,因为 `data` 被就地修改(破坏了稳定引用)
data: data?.filter(d => d.isActive) ?? [],
});
return <table>...</table>;
}
export default function MyComponent() {
//✅ 正确
const columns = useMemo(() => [
// ...
], []);
//✅ 正确(React Query 自动为数据提供稳定引用)
const { data, isLoading } = useQuery({
//...
});
const table = useReactTable({
columns,
//❌ 错误:这将导致无限的重新渲染循环,因为 `data` 被就地修改(破坏了稳定引用)
data: data?.filter(d => d.isActive) ?? [],
});
return <table>...</table>;
}
为了防止无限循环,您应该始终记忆化您的数据转换。这可以使用 useMemo 或类似方法来完成。
export default function MyComponent() {
//✅ 正确
const columns = useMemo(() => [
// ...
], []);
//✅ 正确
const { data, isLoading } = useQuery({
//...
});
//✅ 正确:这不会导致无限的重新渲染循环,因为 `filteredData` 被记忆化了
const filteredData = useMemo(() => data?.filter(d => d.isActive) ?? [], [data]);
const table = useReactTable({
columns,
data: filteredData, // 稳定引用!
});
return <table>...</table>;
}
export default function MyComponent() {
//✅ 正确
const columns = useMemo(() => [
// ...
], []);
//✅ 正确
const { data, isLoading } = useQuery({
//...
});
//✅ 正确:这不会导致无限的重新渲染循环,因为 `filteredData` 被记忆化了
const filteredData = useMemo(() => data?.filter(d => d.isActive) ?? [], [data]);
const table = useReactTable({
columns,
data: filteredData, // 稳定引用!
});
return <table>...</table>;
}
当 React Forget 发布时,这些问题可能成为过去。或者 просто使用 Solid.js... 🤓
大多数插件使用的状态在数据源更改时通常应该重置,但有时如果您正在外部筛选数据,或者在查看数据时不可变地编辑数据,或者只是对数据进行任何您不希望触发表格状态自动重置的外部操作,则需要禁止这种情况发生。
对于这些情况,每个插件都提供了一种在数据或状态的其他依赖项更改时禁用状态自动内部重置的方法。通过将它们中的任何一个设置为 false,您可以阻止触发自动重置。
以下是一个基于 React 的示例,说明了在编辑表格的 data 源时,如何阻止几乎所有状态像通常那样更改:
const [data, setData] = React.useState([])
const skipPageResetRef = React.useRef()
const updateData = newData => {
// 当使用此函数更新数据时,设置一个标志
// 以禁用所有自动重置
skipPageResetRef.current = true
setData(newData)
}
React.useEffect(() => {
// 表格更新后,始终移除标志
skipPageResetRef.current = false
})
useReactTable({
...
autoResetPageIndex: !skipPageResetRef.current,
autoResetExpanded: !skipPageResetRef.current,
})
const [data, setData] = React.useState([])
const skipPageResetRef = React.useRef()
const updateData = newData => {
// 当使用此函数更新数据时,设置一个标志
// 以禁用所有自动重置
skipPageResetRef.current = true
setData(newData)
}
React.useEffect(() => {
// 表格更新后,始终移除标志
skipPageResetRef.current = false
})
useReactTable({
...
autoResetPageIndex: !skipPageResetRef.current,
autoResetExpanded: !skipPageResetRef.current,
})
现在,当我们更新数据时,上述表格状态将不会自动重置!