前端多选操作的那些坑我帮你踩过了

シ迁迁 交互 阅读 1,660
赞 19 收藏
二维码
手机扫码查看
反馈

表格多选功能搞死我了,全选按钮各种bug

最近做一个后台管理界面,表格多选功能把我整得够呛。本来以为就是简单的checkbox操作,结果各种边界情况和状态同步问题让我折腾了两天。

前端多选操作的那些坑我帮你踩过了

最大的问题是全选按钮的逻辑,点击全选后,某些情况下取消某个子项,全选按钮的状态就不对了。还有就是数据更新后,之前选中的状态丢失,用户一脸懵逼。这些都是细节问题,但用户体验很糟糕。

折腾了半天发现,事件绑定和状态管理不能分开考虑

一开始我想得很简单,给每个checkbox绑个click事件,然后统计总数就行。结果发现这种方式有个致命问题:当数据重新渲染时,之前的事件绑定全部失效,需要重新绑定。更要命的是,如果有分页,翻页后还要维护跨页面的选择状态。

这里我踩了个坑。我尝试用原生DOM操作来维护状态,比如用一个数组存储选中的ID,然后每次渲染完表格都遍历一遍更新checkbox的checked属性。代码写得很乱,而且性能很差,每行都要判断一次是否被选中。

后来试了下Vue的双向绑定,感觉会好一些,但还是有些边界情况处理不好。特别是批量删除后,数据变了,但是选中状态的数组没及时更新,导致页面显示错乱。

核心代码就这几行,想通了其实很简单

最后我还是用了React + useState的方式来处理。主要思路是用一个Set来存储选中的ID,这样增删查的效率都很高,而且状态管理也比较清晰。

import React, { useState, useEffect } from 'react';

const TableWithSelection = ({ data }) => {
  const [selectedIds, setSelectedIds] = useState(new Set());
  const [isAllSelected, setIsAllSelected] = useState(false);

  // 监听数据变化,清理不存在的选中项
  useEffect(() => {
    const newDataIds = new Set(data.map(item => item.id));
    const updatedSelectedIds = new Set(
      Array.from(selectedIds).filter(id => newDataIds.has(id))
    );
    setSelectedIds(updatedSelectedIds);
  }, [data]);

  // 单个选择/取消
  const handleSingleSelect = (id, checked) => {
    const newSelectedIds = new Set(selectedIds);
    if (checked) {
      newSelectedIds.add(id);
    } else {
      newSelectedIds.delete(id);
    }
    setSelectedIds(newSelectedIds);
  };

  // 全选/取消全选
  const handleSelectAll = () => {
    if (isAllSelected) {
      // 取消全选,只保留在当前数据中存在的ID
      const currentIds = new Set(data.map(item => item.id));
      const remainingIds = new Set(
        Array.from(selectedIds).filter(id => !currentIds.has(id))
      );
      setSelectedIds(remainingIds);
    } else {
      // 全选,合并现有选中项和当前所有ID
      const allCurrentIds = data.map(item => item.id);
      const newSelectedIds = new Set([...selectedIds, ...allCurrentIds]);
      setSelectedIds(newSelectedIds);
    }
  };

  // 更新全选状态
  useEffect(() => {
    if (data.length === 0) {
      setIsAllSelected(false);
      return;
    }
    
    const currentIds = new Set(data.map(item => item.id));
    const selectedCurrentIds = Array.from(selectedIds).filter(id => 
      currentIds.has(id)
    );
    
    setIsAllSelected(selectedCurrentIds.length > 0 && 
                     selectedCurrentIds.length === data.length);
  }, [selectedIds, data]);

  return (
    <table className="table">
      <thead>
        <tr>
          <th>
            <input
              type="checkbox"
              checked={isAllSelected}
              onChange={handleSelectAll}
            />
          </th>
          <th>Name</th>
          <th>Email</th>
        </tr>
      </thead>
      <tbody>
        {data.map(item => (
          <tr key={item.id}>
            <td>
              <input
                type="checkbox"
                checked={selectedIds.has(item.id)}
                onChange={(e) => handleSingleSelect(item.id, e.target.checked)}
              />
            </td>
            <td>{item.name}</td>
            <td>{item.email}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

这里注意我踩过好几次坑,异步操作容易出问题

上面代码看起来简单,但有几个地方特别容易出错。首先是useEffect的依赖数组,如果忘记加data,那么当外部数据更新时,选中状态不会相应调整。

其次是全选逻辑那块,我之前写错了好几次。取消全选的时候,不能直接清空整个selectedIds,因为可能有其他页面的数据也需要保留。所以要用集合运算来处理,只移除当前页面的ID。

还有一个细节,就是全选状态的判断。要同时满足两个条件:当前页面有数据,且当前页面的选中数量等于当前页面的总数据量。否则会出现空数据时全选按钮也被勾上的诡异现象。

批量操作相关的状态同步也挺复杂

做完了基本的选择逻辑,接下来就是配合实际业务的批量操作。删除选中项后,对应的选中状态也要清除,否则下次渲染就会有问题。

// 批量删除
const handleBatchDelete = async () => {
  if (selectedIds.size === 0) return;
  
  try {
    await fetch('https://jztheme.com/api/users/batch-delete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ids: Array.from(selectedIds) })
    });
    
    // 删除成功后,清空选中状态
    const newSelectedIds = new Set(
      Array.from(selectedIds).filter(id => 
        !data.some(item => item.id === id)
      )
    );
    setSelectedIds(newSelectedIds);
    
    // 刷新数据
    refreshData();
  } catch (error) {
    console.error('Batch delete failed:', error);
  }
};

// 获取选中项的操作
const getSelectedItems = () => {
  return data.filter(item => selectedIds.has(item.id));
};

性能优化方面也做了些调整

数据量大的时候,频繁的状态更新会影响性能。我给select相关的操作加了个防抖,避免连续点击造成的状态冲突。

另外Set比Array在查找操作上确实快不少,特别是在判断某个ID是否被选中时。原来我用includes方法,数据多了以后明显感觉到卡顿。

还有个小技巧,在渲染大量checkbox时,可以用React.memo来优化子组件,避免不必要的重渲染。不过在这个场景下,由于涉及到批量状态更新,直接用memo可能不太合适,需要仔细评估。

踩坑提醒:跨页面选择功能要谨慎

最开始产品经理说要做跨页面选择,就是用户可以在不同页面间切换,选中的项目能够累积。理论上可行,但实际用起来非常容易误操作,用户经常忘记自己在别的页面选了什么,提交时发现选了一堆不该选的内容。

后来我们改成了页面内选择的模式,每个页面独立管理选中状态。虽然功能上简单了些,但用户体验反而更好,出错率也降低了。

唯一的小问题是,如果用户需要跨页面批量操作,就得分多次进行。不过这个妥协是可以接受的,毕竟正确的操作比便利性更重要。

移动端的多选体验也需要注意

桌面端用鼠标点checkbox还好,移动端体验就很一般。后来加了长按进入选择模式的功能,让用户可以选择多个项目,而不是每次都点checkbox。

长按逻辑其实也不复杂,就是监听onTouchStart事件,然后启动一个定时器。如果用户长按超过500ms就进入多选模式,这时候再点击行就可以选择,而不是触发默认的行点击行为。

不过这个交互模式要给用户明确的视觉提示,比如选中状态的样式要更明显,选中计数要在顶部显示等。

以上是我踩坑后的总结,希望对你有帮助。多选功能看似简单,但要把各种边界情况都处理好,还是要花不少心思的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论