react-draggable、react-grid-layout 和 react-resizable 是 React 生态系统中用于构建交互式 UI 的核心底层库。react-draggable 专注于让单个元素在容器内自由拖拽,提供基础的鼠标/触摸事件处理。react-resizable 则专门负责让元素可以通过手柄调整大小,支持多方向缩放。react-grid-layout 是一个更高级的布局引擎,它结合了前两者的能力,实现了类似 Gridster 的响应式网格系统,支持拖拽、调整大小、自动整理和序列化布局状态。这三个库通常由同一维护者开发,API 风格一致,常配合使用以构建复杂的仪表盘或编辑器界面。
在构建现代 React 应用时,我们经常需要实现用户界面的高度交互性,比如可拖拽的卡片、可调整大小的侧边栏,或者是完全可定制的仪表盘。react-draggable、react-resizable 和 react-grid-layout 是解决这些问题的三个关键库。虽然它们经常一起出现,甚至由同一个作者维护,但它们的职责划分非常清晰。让我们深入看看它们各自的技术特点和应用场景。
这三个库的设计哲学截然不同。react-draggable 和 react-resizable 遵循“单一职责原则”,分别只解决拖拽和调整大小一个问题。而 react-grid-layout 是一个系统级的解决方案,它内部实际上组合了前两者的逻辑,并增加了网格碰撞检测和布局管理。
react-draggable 让你把任何组件变成可拖拽的。它不关心组件在哪里,只关心相对位移。
import Draggable from 'react-draggable';
function MyDraggableComponent() {
return (
<Draggable>
<div className="box">拖拽我 anywhere</div>
</Draggable>
);
}
react-resizable 给组件加上调整大小的手柄。它不关心组件怎么移动,只关心尺寸变化。
import { ResizableBox } from 'react-resizable';
function MyResizableComponent() {
return (
<ResizableBox width={200} height={200} minConstraints={[100, 100]}>
<div className="box">调整我的大小</div>
</ResizableBox>
);
}
react-grid-layout 管理整个网格系统。它强制元素对齐网格,处理元素之间的碰撞,并自动整理布局。
import GridLayout from 'react-grid-layout';
const ResponsiveGridLayout = GridLayout.Responsive;
function MyDashboard() {
const layout = [{ i: 'a', x: 0, y: 0, w: 2, h: 2 }];
return (
<ResponsiveGridLayout className="layout" cols={12} rowHeight={30}>
<div key="a">网格项 A (自动对齐)</div>
</ResponsiveGridLayout>
);
}
这是选择库时最关键的分水岭。你是需要像素级的自由移动,还是基于网格的结构化布局?
react-draggable 提供绝对的自由。元素可以停留在容器的任何像素位置。它通过 position 样式或 transform 来实现移动,完全不依赖网格系统。
// react-draggable: 自由定位
<Draggable defaultPosition={{ x: 55, y: 120 }}>
<div>我可以停在任意坐标 (55, 120)</div>
</Draggable>
react-resizable 同样不依赖网格。它直接修改元素的 width 和 height 样式。你可以将其与 react-draggable 组合使用,实现自由移动且自由缩放的面板,但这需要你自己处理两者的状态同步。
// react-resizable: 自由缩放
<ResizableBox width={300} height={150}>
<div>尺寸完全由用户决定,无网格限制</div>
</ResizableBox>
react-grid-layout 强制所有操作基于网格。当你拖拽一个元素时,它会自动吸附到最近的网格线;当你调整大小时,宽高的变化必须是网格单位(column width 和 row height)的整数倍。
// react-grid-layout: 网格约束
// 元素只能占据整数个网格单元,如 w=2 (占2列), h=3 (占3行)
const layoutItem = { i: 'b', x: 2, y: 0, w: 4, h: 2 };
// 无法设置 w=4.5 或 x=2.3,系统会自动修正
当多个元素在同一个容器中时,它们是否会互相干扰?如果需要互不重叠,谁负责计算?
react-draggable 没有内置碰撞检测。如果你拖拽一个元素覆盖另一个,它们会重叠。你需要自己编写逻辑来检测坐标冲突并阻止移动,这在多元素场景下非常复杂。
// react-draggable: 无碰撞检测
// 如果两个 Draggable 重叠,它们会叠在一起
// 开发者必须手动监听 onDrag 并计算边界盒 (Bounding Box) 来防止重叠
<Draggable onDrag={(e, data) => {
// 需要手动编写复杂的逻辑来检查是否与其他元素重叠
if (checkCollision(data)) return false;
}}>
<div>可能会重叠</div>
</Draggable>
react-resizable 同样没有碰撞检测。调整大小时,元素可能会覆盖旁边的内容。它只关注自身尺寸的变化边界。
// react-resizable: 无碰撞检测
// 放大时可能会覆盖相邻元素,库本身不阻止
<ResizableBox onResize={(e, { size }) => {
// 需要手动检查 size 是否侵占了其他元素的空间
}}>
<div>可能会覆盖别人</div>
</ResizableBox>
react-grid-layout 的核心优势就是自动碰撞检测和整理 (Compact)。当你移动一个元素到已有元素的位置时,其他元素会自动推开(向下或向左/右)。当删除一个元素后,剩余元素会自动填补空缺。
// react-grid-layout: 自动碰撞处理
// 当元素 A 移动到元素 B 的位置,B 会自动下移
// 无需编写任何碰撞检测代码
<ResponsiveGridLayout compactType="vertical">
<div key="1">自动推开其他元素</div>
<div key="2">自动填补空缺</div>
</ResponsiveGridLayout>
在真实应用中,我们通常需要保存用户的布局偏好。不同库的状态结构复杂度不同。
react-draggable 的状态非常简单,通常只是一个 { x, y } 坐标对象。你可以轻松将其存入数据库或 LocalStorage。
// react-draggable: 简单的坐标状态
const [position, setPosition] = useState({ x: 0, y: 0 });
<Draggable position={position} onStop={(e, data) => setPosition({ x: data.x, y: data.y })}>
<div>保存 {JSON.stringify(position)}</div>
</Draggable>
react-resizable 的状态是 { width, height }。同样非常直观,易于序列化。
// react-resizable: 简单的尺寸状态
const [size, setSize] = useState({ width: 200, height: 200 });
<ResizableBox width={size.width} height={size.height} onResizeStop={(e, { size }) => setSize(size)}>
<div>保存 {JSON.stringify(size)}</div>
</ResizableBox>
react-grid-layout 的状态是一个布局数组,每个元素包含 i (ID), x, y, w, h 等多个属性。它还支持多断点(响应式)布局,状态结构更复杂,但信息量更大。
// react-grid-layout: 复杂的布局数组状态
const [layout, setLayout] = useState([
{ i: 'a', x: 0, y: 0, w: 2, h: 2 },
{ i: 'b', x: 2, y: 0, w: 4, h: 4 }
]);
<ResponsiveGridLayout layout={layout} onLayoutChange={(newLayout) => setLayout(newLayout)}>
{/* 内容 */}
</ResponsiveGridLayout>
// 保存时通常直接序列化整个 layout 数组
localStorage.setItem('dashboard-layout', JSON.stringify(layout));
虽然 react-grid-layout 功能强大,但在某些混合场景下,你可能需要组合使用这些库。例如,在一个网格布局中,某个特定的卡片内部需要一个可自由拖拽的小工具,或者一个非网格区域的侧边栏需要调整大小。
场景:网格内的自由元素
你可以在 react-grid-layout 的网格项内部嵌套 react-draggable。注意,内部的拖拽是相对于网格项容器的,而不是整个页面。
// 组合使用:网格中包含自由拖拽元素
<ResponsiveGridLayout>
<div key="widget-container">
<h3>网格卡片</h3>
{/* 在这个网格项内部,用户可以自由拖拽这个小按钮 */}
<Draggable bounds="parent">
<div className="floating-tool">自由移动的工具</div>
</Draggable>
</div>
</ResponsiveGridLayout>
场景:独立的侧边栏 + 主网格区
页面左侧是一个可调整大小的侧边栏(使用 react-resizable),右侧是主仪表盘(使用 react-grid-layout)。两者互不干扰,状态独立管理。
// 组合使用:侧边栏 + 主网格
<div style={{ display: 'flex' }}>
{/* 左侧:独立的可调整大小区域 */}
<ResizableBox width={250} height={window.innerHeight} axis="x">
<Sidebar />
</ResizableBox>
{/* 右侧:主网格布局 */}
<div style={{ flex: 1 }}>
<ResponsiveGridLayout>
<div key="main-chart">主图表</div>
<div key="data-table">数据表</div>
</ResponsiveGridLayout>
</div>
</div>
在使用这些库时,有几个技术细节容易导致 Bug:
CSS 冲突:react-draggable 和 react-resizable 依赖特定的 CSS 类来处理光标样式和手柄位置。如果不小心覆盖了这些样式(例如全局 CSS 重置),会导致拖拽失效或手柄不可见。务必引入库提供的默认 CSS 或仔细自定义。
Transform vs Position:react-draggable 默认使用 transform: translate(...) 进行移动,这对性能更好,但可能会与某些 CSS 属性(如 fixed 定位的父级)产生冲突。如果需要,可以通过 useTransform={false} 切换回 top/left 定位。
// 解决固定父级导致的坐标问题
<Draggable useTransform={false} positionOffset={{ x: 0, y: 0 }}>
<div>使用 top/left 而非 transform</div>
</Draggable>
响应式断点计算:在 react-grid-layout 中,断点切换时布局可能会跳动。确保正确配置 breakpoints 和 cols 数组,并在容器宽度变化时触发 onLayoutChange 以保存最新状态。
Z-Index 管理:当多个元素拖拽或调整大小时,被操作的元素需要浮在其他元素之上。react-grid-layout 会自动处理 zIndex,但如果你手动组合 react-draggable,可能需要自己管理 onStart 时提升层级,onStop 时恢复层级。
| 特性 | react-draggable | react-resizable | react-grid-layout |
|---|---|---|---|
| 核心用途 | 单元素自由拖拽 | 单元素自由缩放 | 多元素网格布局系统 |
| 定位方式 | 绝对像素 (Absolute) | N/A (尺寸控制) | 网格坐标 (Grid Units) |
| 碰撞检测 | ❌ 无 (需手写) | ❌ 无 (需手写) | ✅ 自动处理 (Auto-compact) |
| 状态结构 | { x, y } | { width, height } | [{ i, x, y, w, h }, ...] |
| 响应式支持 | ❌ 需手写媒体查询 | ❌ 需手写媒体查询 | ✅ 内置多断点支持 |
| 依赖关系 | 无 | 无 | 依赖 react-draggable 和 react-resizable 的逻辑 |
| 典型场景 | 模态框、浮动按钮 | 侧边栏、分割面板 | 仪表盘、低代码编辑器 |
选择哪个库,取决于你对“秩序”的需求程度:
如果你需要绝对的自由,元素之间互不干涉,或者只是做一个简单的拖拽功能,react-draggable 和 react-resizable 是你的最佳搭档。它们轻量、灵活,可以按需组合。
如果你需要结构化的布局,元素之间需要自动避让,且需要保存复杂的排列状态(如 SaaS 仪表盘),react-grid-layout 是唯一的选择。它虽然学习曲线稍陡,但能为你省去大量处理碰撞和响应式的底层代码。
在实际工程中,很多高级应用会同时使用这三者:用 react-grid-layout 搭建主框架,用 react-resizable 做外围的可变容器,用 react-draggable 实现局部的微交互。理解它们的边界,才能灵活驾驭。
当你的核心需求是让用户能够调整组件的尺寸(如侧边栏、可缩放面板或图像裁剪框)时,选择 react-resizable。它提供了精细的控制手柄(handles),支持限制最小/最大尺寸和锁定宽高比。如果不需要拖拽功能,或者你打算自己组合拖拽逻辑,它是一个专注且高效的底层构建块。
当你需要构建一个完整的仪表盘、可配置的桌面环境或复杂的网格编辑器时,选择 react-grid-layout。它将拖拽和调整大小功能整合在一个统一的网格系统中,自动处理元素间的碰撞、自动整理(compact)和响应式断点。如果你的应用需要保存和恢复布局状态,或者需要多个组件在网格中协同工作,这是唯一合适的选择。
当你只需要让单个组件(如模态框、浮动工具栏或卡片)在页面自由移动时,选择 react-draggable。它轻量且无外部依赖,适合不需要网格对齐或自动布局的场景。如果你的需求仅限于“拖拽”这一单一行为,且不需要调整大小或复杂的网格碰撞检测,这是最直接的选择。
A simple widget that can be resized via one or more handles.
You can either use the <Resizable> element directly, or use the much simpler <ResizableBox> element.
See the example and associated code in ExampleLayout and ResizableBox for more details.
$ npm install --save react-resizable
You must include the associated styles in your application, otherwise the resize handles will not be visible and will not work properly.
// In your JS/TS entry point:
import 'react-resizable/css/styles.css';
Or import it in your CSS:
@import 'react-resizable/css/styles.css';
If you're using a bundler that doesn't support CSS imports, you can find the styles at node_modules/react-resizable/css/styles.css and include them manually.
As of 4.0.0, the library is authored in TypeScript and ships bundled type
declarations in build/*.d.ts. You do not need to install
@types/react-resizable; if you previously installed it, remove it so the
bundled types take precedence:
npm uninstall @types/react-resizable
# or
yarn remove @types/react-resizable
Public types are re-exported from the package root:
import {
Resizable,
ResizableBox,
// types
type ResizeCallbackData,
type ResizeHandleAxis,
type Axis,
type Props as ResizableProps,
} from 'react-resizable';
Flow is no longer supported as of 4.0.0. Earlier versions shipped
*.js.flow sidecar files generated from the Flow-annotated source; those
have been removed.
If you still need Flow types, you can vendor the last Flow-annotated source
locally from the 3.2.0 tag.
They will not be updated to reflect changes landing after 4.0.0. The
official recommendation is to migrate to TypeScript.
| Version | React Version | Types |
|---|---|---|
| 4.x | >= 16.3 | TypeScript (bundled) |
| 3.x | >= 16.3 | Flow (*.js.flow) |
| 2.x | Skipped | — |
| 1.x | 14 - 17 | Flow |
This package has two major exports:
<Resizable>: A raw component that does not have state. Use as a building block for larger components, by listening to its callbacks and setting its props.<ResizableBox>: A simple <div {...props} /> element that manages basic state. Convenient for simple use-cases.<Resizable>import { Resizable } from 'react-resizable';
import 'react-resizable/css/styles.css';
class Example extends React.Component {
state = {
width: 200,
height: 200,
};
onResize = (event, {node, size, handle}) => {
this.setState({width: size.width, height: size.height});
};
render() {
return (
<Resizable
height={this.state.height}
width={this.state.width}
onResize={this.onResize}
>
<div
className="box"
style={{width: this.state.width + 'px', height: this.state.height + 'px'}}
>
<span>Contents</span>
</div>
</Resizable>
);
}
}
<ResizableBox>import { ResizableBox } from 'react-resizable';
import 'react-resizable/css/styles.css';
class Example extends React.Component {
render() {
return (
<ResizableBox
width={200}
height={200}
draggableOpts={{grid: [25, 25]}}
minConstraints={[100, 100]}
maxConstraints={[300, 300]}
>
<span>Contents</span>
</ResizableBox>
);
}
}
These props apply to both <Resizable> and <ResizableBox>. Unknown props that are not in the list below will be passed to the child component.
type ResizeCallbackData = {
node: HTMLElement;
size: {width: number; height: number};
handle: ResizeHandleAxis;
};
type ResizeHandleAxis = 's' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne';
type ResizableProps = {
children: React.ReactElement<any>;
width: number;
height: number;
// Either a ReactElement to be used as handle, or a function
// returning an element that is fed the handle's location as its first argument.
handle?:
| React.ReactElement<any>
| ((resizeHandle: ResizeHandleAxis, ref: React.RefObject<HTMLElement>) => React.ReactElement<any>);
// If you change this, be sure to update your css. Default: [20, 20].
handleSize?: [number, number];
lockAspectRatio?: boolean; // default: false
axis?: 'both' | 'x' | 'y' | 'none'; // default: 'both'
minConstraints?: [number, number]; // default: [20, 20]
maxConstraints?: [number, number]; // default: [Infinity, Infinity]
onResizeStop?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any;
onResizeStart?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any;
onResize?: (e: React.SyntheticEvent, data: ResizeCallbackData) => any;
// Forwarded to react-draggable's <DraggableCore>.
draggableOpts?: Partial<React.ComponentProps<typeof import('react-draggable').DraggableCore>>;
resizeHandles?: ResizeHandleAxis[]; // default: ['se']
// If `transform: scale(n)` is set on the parent, this should be set to `n`.
transformScale?: number; // default: 1
};
The following props can also be used on <ResizableBox>:
{
style?: React.CSSProperties; // styles the returned <div />
}
If a width or height is passed to <ResizableBox>'s style prop, it will be ignored as it is required for internal function.
You can pass options directly to the underlying DraggableCore instance by using the prop draggableOpts. See the demo for more on this.
If you override the resize handle, we expect that any ref passed to your new handle will represent the underlying DOM element.
This is required, as react-resizable must be able to access the underlying DOM node to attach handlers and measure position deltas.
There are a few ways to do this:
This requires no special treatment.
<Resizable handle={<div className="foo" />} />
You must forward the ref and props to the underlying DOM element.
class MyHandleComponent extends React.Component {
render() {
const {handleAxis, innerRef, ...props} = this.props;
return <div ref={innerRef} className={`foo handle-${handleAxis}`} {...props} />
}
}
const MyHandle = React.forwardRef((props, ref) => <MyHandleComponent innerRef={ref} {...props} />);
<Resizable handle={<MyHandle />} />
const MyHandle = React.forwardRef((props, ref) => {
const {handleAxis, ...restProps} = props;
return <div ref={ref} className={`foo handle-${handleAxis}`} {...restProps} />;
});
<Resizable handle={<MyHandle />} />
You can define a function as a handle, which will simply receive an axis (see above ResizeHandleAxis type) and ref. This may be more clear to read, depending on your coding style.
const MyHandle = (props) => {
return <div ref={props.innerRef} className="foo" {...props} />;
};
<Resizable handle={(handleAxis, ref) => <MyHandle innerRef={ref} className={`foo handle-${handleAxis}`} />} />
MIT