You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:21:40