React分页表格复选框索引绑定导致跨页误选中解决方案
分页表格复选框串位问题修复方案
问题根因
原有实现的选中状态完全绑定列表渲染的数组索引,没有和用户数据的唯一标识关联:切换分页后,新一页的用户数据从索引0开始顺序渲染,会直接复用旧索引对应的选中状态,导致同位置复选框异常选中。同时原有布尔数组的状态结构长度和当前页数据量不匹配,也会导致全选、半选逻辑判断异常。
修复思路
将选中状态的存储逻辑从「索引对应布尔值」改为「存储已选中的用户唯一ID」,彻底和渲染索引解绑,既解决切页串位问题,也可原生支持跨页多选场景。如果业务不需要跨页保留选中状态,只需在页码变化时清空选中ID集合即可。
具体修改点
- 替换
checkedItems的状态类型:从布尔数组改为Set<string>,初始值为空集合,专门存储已选中的用户ID - 调整全选/半选判断逻辑:遍历当前页所有用户ID,判断是否全部存在于选中集合中,以此计算全选状态和半选状态
- 重写表头全选框事件:勾选时将当前页所有用户ID加入选中集合,取消勾选时将当前页所有用户ID从选中集合中移除
- 重写单行复选框逻辑:直接判断当前用户ID是否在选中集合中控制选中状态,勾选/取消时直接操作集合中对应用户ID,完全不依赖数组索引
- (可选)监听页码变化,若不需要跨页保留选中,切页时直接清空选中集合
修复后完整代码
import { useUsers } from '../../services/hooks/useUsers'; import { Box, Button, Checkbox, Flex, Heading, Icon, Table, Tbody, Td, Th, Thead, Tr, Text, useBreakpointValue, Spinner, Link, HStack, useDisclosure, } from '@chakra-ui/react'; import { Header } from '../../components/Header'; import { NotificationModal } from '../../components/NotificationModal'; import { Sidebar } from '../../components/Sidebar'; import { RiAddLine } from 'react-icons/ri'; import { CgImport } from 'react-icons/cg'; import { TbEdit } from 'react-icons/tb'; import { FaWhatsapp } from 'react-icons/fa'; import { CgNotes } from 'react-icons/cg'; import { Pagination } from '../../components/Pagination'; import NextLink from 'next/link'; import { useState, useEffect } from 'react'; import { queryClient } from '../../services/queryClient'; import { api } from '../../services/api'; export default function UserList() { const [page, setPage] = useState(1); const { data, isLoading, isFetching, error } = useUsers(page); // 替换原有布尔数组为存储选中用户ID的Set const [checkedUserIds, setCheckedUserIds] = useState<Set<string>>(new Set()); // 计算当前页全选/半选状态 const currentPageUserIds = data?.users.map(user => user.id) || []; const allChecked = currentPageUserIds.length > 0 && currentPageUserIds.every(id => checkedUserIds.has(id)); const isIndeterminate = currentPageUserIds.some(id => checkedUserIds.has(id)) && !allChecked; // 切页时如果不需要跨页保留选中,打开下面这段注释即可 // useEffect(() => { // setCheckedUserIds(new Set()) // }, [page]) async function handlePrefetchUser(userId: string) { await queryClient.prefetchQuery( ['user', userId], async () => { const response = await api.get(`users/${userId}`); return response.data; }, { staleTime: 1000 * 60 * 10, } ); } // 全选/取消全选当前页处理 const handleToggleAllCurrentPage = (checked: boolean) => { setCheckedUserIds(prev => { const next = new Set(prev); if (checked) { currentPageUserIds.forEach(id => next.add(id)); } else { currentPageUserIds.forEach(id => next.delete(id)); } return next; }) } // 单行选中切换处理 const handleToggleUser = (userId: string, checked: boolean) => { setCheckedUserIds(prev => { const next = new Set(prev); if (checked) { next.add(userId); } else { next.delete(userId); } return next; }) } return ( <Box> <Header /> <Flex my="6" maxWidth={1480} mx="auto" w="94%"> <Sidebar /> <Box flex="1" borderRadius={8} bg="gray.800" p="8" overflow="hidden"> <Flex mb="8" justify="space-between" align="center"> <Heading size="lg" fontWeight="normal"> Alunos {!isLoading && isFetching && ( <Spinner size="sm" color="gray.500" ml="4" /> )} </Heading> <HStack spacing={4}> <NextLink href="/users/create" passHref> <Button as="a" size="sm" fontSize="sm" colorScheme="orange"> <Icon as={RiAddLine} fontSize="20" /> </Button> </NextLink> <NotificationModal /> <Button as="a" size="sm" fontSize="sm" colorScheme="blue" cursor="pointer" > <Icon as={CgImport} fontSize="20" /> </Button> </HStack> </Flex> {isLoading ? ( <Flex justify="center" align="center"> <Spinner /> </Flex> ) : error ? ( <Flex justify="center"> <Text>Falha ao obter dados dos usuários.</Text> </Flex> ) : ( <> <Table colorScheme="whiteAlpha" overflow="none" size="md"> <Thead> <Tr> <Th px={['4', '4', '6']} color="gray.300"> <Checkbox colorScheme="orange" isChecked={allChecked} isIndeterminate={isIndeterminate} onChange={(e) => handleToggleAllCurrentPage(e.target.checked)} /> </Th> <Th w="100%">Alunos</Th> <Th>Ações</Th> </Tr> </Thead> <Tbody> {data.users.map((user) => ( <Tr key={user.id}> <Td px={['4', '4', '6']}> <Checkbox colorScheme="orange" isChecked={checkedUserIds.has(user.id)} onChange={(e) => handleToggleUser(user.id, e.target.checked)} /> </Td> <Td> <Box> <Link color="orange.400" onMouseEnter={() => handlePrefetchUser(user.id)} > <Text fontWeight="bold" fontSize={['sm', 'md']}> {user.name} </Text> </Link> </Box> </Td> <Td> <Box cursor="pointer"> <HStack spacing={6}> <Icon as={TbEdit} color="gray.400" fontSize="22" _hover={{ color: 'white', }} /> <Icon as={FaWhatsapp} color="gray.400" fontSize="22" _hover={{ color: 'white', }} /> <Icon as={CgNotes} color="gray.400" fontSize="22" _hover={{ color: 'white', }} /> </HStack> </Box> </Td> </Tr> ))} </Tbody> </Table> <Pagination totalCountOfRegisters={data.totalCount} currentPage={page} onPageChange={setPage} /> </> )} </Box> </Flex> </Box> ); }
内容的提问来源于stack exchange,提问作者jayker
相关产品推荐
相关产品推荐

