react-beautiful-dnd表格拖拽时行收缩 问题解决

本文首发于 react-beautiful-dnd表格拖拽时行收缩 - 凝结尾迹

最近在使用react-beautiful-dnd开发表格拖拽排序时遇到了一个问题:在选中一行并开始拖拽时 ,行、或者说单元格的样式会发生变化,具体表现为整行宽度收缩。在搜索资料并翻阅文档后,找到了这个问题的解决方法。

先看一下对一个标准表格的某行拖拽时会发生什么吧,可以看到被拖拽行收缩了起来,这实际上是因为列的宽度丢失了。

在react-beautiful-dnd官方文档中提到了在表格中使用的方法:Tables。其中提到可以使用两种策略对表格进行拖拽排序,分别是Fixed layoutsDimension locking,接下来我分别介绍一下。

Fixed layouts

相比于后者,Fixed layouts性能更好且更容易实现,但是只适用于表格列宽固定的情况,这种情况下,只需要为<Draggable />包裹的行设置display: table即可。

如果上述方法不生效,也可以直接为<td>元素设置一个固定宽度,比如这里将<td>元素的宽度设置为120px

jsx 复制代码
<DragDropContext onDragEnd={this.onDragEnd}>
  <Droppable droppableId="droppable">
    {(provided, snapshot) => (
      <table
        ref={provided.innerRef}
        style={getListStyle(snapshot.isDraggingOver)}
      >
        <thead>
          <tr>
            <th>Title</th>
            <th>Test</th>
          </tr>
        </thead>
        <tbody>
          {this.state.items.map((item, index) => (
            <Draggable key={item.id} draggableId={item.id} index={index}>
              {(provided, snapshot) => (
                <tr
                  ref={provided.innerRef}
                  {...provided.draggableProps}
                  {...provided.dragHandleProps}
                  style={getItemStyle(
                    snapshot.isDragging,
                    provided.draggableProps.style
                  )}
                >
                  <td style={{ width: "120px" }}>{item.content}</td>
                  <td style={{ width: "120px" }}>{item.test}</td>
                </tr>
              )}
            </Draggable>
          ))}
          {provided.placeholder}
        </tbody>
      </table>
    )}
  </Droppable>
</DragDropContext>

当然这不是本文的重点,毕竟不是每个表格都可以固定列宽的,很多情况下列宽会根据单元格中的内容自适应,这种情况就只能使用下面的方法了。

Dimension locking

前面提到这种方法适用于列宽根据内容自适应的情况,不仅如此,它同样适用于列宽固定的情况,而且更加具有健壮性,但是性能会比较差。使用这种方法时表格内容最好不要超过50行,就算不考虑性能,上百行内容的拖拽体验想必也不会很好。

这个方法的实现思路简单来说就是:在拖拽前记录被拖拽行每个单元格的原始宽度和高度,并在拖拽中将该行单元格宽高设置为记录值,拖拽结束后移除样式。

jsx 复制代码
import React, { useState, useEffect, useRef } from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

class LockedCell extends React.Component {
  ref;

  getSnapshotBeforeUpdate(prevProps) {
    if (!this.ref) {
      return null;
    }

    const isDragStarting =
      this.props.isDragOccurring && !prevProps.isDragOccurring;

    if (!isDragStarting) {
      return null;
    }

    const { width, height } = this.ref.getBoundingClientRect();

    const snapshot = {
      width,
      height,
    };

    return snapshot;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    const ref = this.ref;
    if (!ref) {
      return;
    }

    if (snapshot) {
      if (ref.style.width === snapshot.width) {
        return;
      }
      ref.style.width = `${snapshot.width}px`;
      ref.style.height = `${snapshot.height}px`;
      return;
    }

    if (this.props.isDragOccurring) {
      return;
    }

    // inline styles not applied
    if (ref.style.width == null) {
      return;
    }

    // no snapshot and drag is finished - clear the inline styles
    ref.style.removeProperty("height");
    ref.style.removeProperty("width");
  }

  setRef = (ref) => {
    this.ref = ref;
  };

  render() {
    return (
      <td ref={this.setRef} style={{ boxSizing: "border-box" }}>
        {this.props.children}
      </td>
    );
  }
}

const App = () => {
  const [items, setItems] = useState([]);
  const [isDragging, setIsDragging] = useState(false);

  // 构造测试数据
  const getItems = (count) =>
    Array.from({ length: count }, (v, k) => k).map((k) => ({
      id: `item-${k}`,
      content: `Item ${k}`,
    }));
  useEffect(() => {
    setItems(getItems(3));
  }, []);

  const onDragEnd = (result) => {
    setIsDragging(false);
  };

  const onBeforeDragStart = () => {
    setIsDragging(true);
  };

  return (
    <div style={{ padding: "2rem" }}>
      <table>
        <thead>
          <tr>
            <th>Item</th>
            <th>Two</th>
            <th>Three</th>
            <th>Four</th>
          </tr>
        </thead>

        <DragDropContext
          onDragEnd={onDragEnd}
          onBeforeDragStart={onBeforeDragStart} // DIMENSION LOCKING
        >
          <Droppable droppableId="droppable">
            {(provided) => (
              <tbody {...provided.droppableProps} ref={provided.innerRef}>
                {items.map((item, index) => (
                  <Draggable key={item.id} draggableId={item.id} index={index}>
                    {(provided, snapshot) => (
                      <tr
                        ref={provided.innerRef}
                        {...provided.draggableProps}
                        {...provided.dragHandleProps}
                      >
                        <LockedCell
                          isDragOccurring={isDragging}
                          snapshot={snapshot}
                        >
                          {item.content}
                        </LockedCell>
                        <LockedCell
                          isDragOccurring={isDragging}
                          snapshot={snapshot}
                        >
                          2
                        </LockedCell>
                        <LockedCell
                          isDragOccurring={isDragging}
                          snapshot={snapshot}
                        >
                          3
                        </LockedCell>
                        <LockedCell
                          isDragOccurring={isDragging}
                          snapshot={snapshot}
                        >
                          4
                        </LockedCell>
                      </tr>
                    )}
                  </Draggable>
                ))}
                {provided.placeholder}
              </tbody>
            )}
          </Droppable>
        </DragDropContext>
      </table>
    </div>
  );
};

export default App;

可以看到拖拽容器的实现没有什么变化,关键点在于使用<LockedCell>替代了<td>,而<LockedCell>具体做了什么呢?

刚才已经大致描述过了,使用onBeforeDragStart检测拖拽状态的临界点,拖拽前记录好该行中所有单元格的宽高,拖拽中设置宽高为记录值,拖拽结束后清除样式。这也是本方法性能低的原因,需要频繁读取DOM元素的属性并渲染。

代码的实现可以在这里查看👉 在线代码,最后看一下效果吧,可以看到行拖拽过程中宽度不再收缩了:

相关推荐
随笔记21 分钟前
react-router里的两种路由方式有什么不同
前端·react.js
晴空雨3 小时前
React 合成事件原理:从事件委托到 React 17 的重大改进
前端·react.js
@大迁世界3 小时前
useCallback 的陷阱:当 React Hooks 反而拖了后腿
前端·javascript·react.js·前端框架·ecmascript
Fantastic_sj3 小时前
React 19 核心特性
前端·react.js·前端框架
小高0073 小时前
📌React 路由超详解(2025 版):从 0 到 1 再到 100,一篇彻底吃透
前端·javascript·react.js
wwy_frontend4 小时前
React性能优化实战:从卡顿到丝滑的8个技巧
前端·react.js
ikonan7 小时前
译:不要过度优化你的优化
前端·javascript·react.js
mit6.8248 小时前
[AI React Web] 包与依赖管理 | `axios`库 | `framer-motion`库
前端·人工智能·react.js
晓得迷路了9 小时前
栗子前端技术周刊第 94 期 - React Native 0.81、jQuery 4.0.0 RC1、Bun v1.2.20...
前端·javascript·react.js
江城开朗的豌豆10 小时前
React状态更新踩坑记:我是这样优雅修改参数的
前端·javascript·react.js