use-dnd
is a drag-and-drop library for React heavily based on react-dnd
, but with an emphasis HTML5 drag-and-drop. use-dnd
is not a drop-in replacement for react-dnd
, but it has been designed to require minimal effort to switch over.
- Near feature parity with
react-dnd
'sHTML5Backend
, includinguseDrag
/useDrop
hooks- Custom drag handles
- Custom drag preview
- Drag layers with
useDragLayer
- Support for foreign objects (e.g. dragging from one window to another)
- Direct access to DOM events
- Allows for fine-grained control over drop effect
- Extremely light (~3kB bundled and gzipped)
use-dnd
is available on NPM. Install it with:
npm i use-dnd
Or the equivalent for your package manager of choice.
use-dnd
is ESM-only.
The NPM bundle is much larger than the final bundled output would be with something like webpack because it is not minified and includes the TypeScript source and source maps. While the exact bundled size may vary slightly, it should be quite small. Based on estimates from pnpm analyze
in ./demo
, the final minified and gzipped size will probably be around 3kB.
use-dnd
comes with the TypeScript sources and source maps for them. If you are using a bundler like webpack which performs its own transformations to the code, you may need to configure your bundler to provide you with the source maps (e.g. in webpack you would use source-map-loader
, see ./demo/next.config.js
for an example).
If you're using the react-hooks/exhaustive-deps
ESLint rule (as you should be), configure it to include useDrag
and useDrop
in additionalHooks
:
{
"rules": {
"react-hooks/exhaustive-deps": [
"warn",
{
"additionalHooks": "^(useDrop|useDrag)$"
}
]
}
}
If you need touch support you should use react-dnd
instead. It may be possible to combine use-dnd
and react-dnd
in order to get touch support on mobile devices while keeping some of the nicer feature from use-dnd
like foreign object support. Alternatively, you may want to use a more batteries-included drag-and-drop library like dnd-kit
.
If you need keyboard support to be included out-of-the-box, you should use a different drag-and-drop library like dnd-kit
.
Additionally, if you simply don't need the extra features that use-dnd
provides over other libraries (such as foreign objects) and are willing to take a small size penalty for a more abstracted API, another drag-and-drop library may be a better choice.
Feel free to file bug reports or feature requests in the GitHub issues page. For the best response when filing bug reports, please be as specific as possible and provide a minimal reproducible example. For feature requests, please explain why you would find the feature valuable.
use-dnd
uses pnpm, which is probably easiest to install with npm i -g pnpm
or corepack enable
(Corepack comes with recent Node.js versions). Once pnpm is installed, run pnpm i
to install use-dnd
's dependencies.
To test changes, go into the demo
directory and run pnpm i
, then pnpm dev
. This will start up a Next.js dev server which will allow you to make changes with hot reload. Also run pnpm watch
in the root directory so that use-dnd
will be automatically recompiled when the source files are changed.
use-dnd
uses ESLint, which can be run with pnpm lint
.
Feel free to make a pull request with bug fixes. New features may be accepted directly from PR, but if you want to make sure then please make an issue about it first. You may state in your issue that you are willing to put in the development time yourself to let me know that the feature request would not require significant development work on my part.
use-dnd
provides types for items based on the item type. Add your own types through module augmentation:
declare module 'use-dnd' {
interface DragTypeItemContentMap {
'application/my-drag-type+json': MyItem;
'application/my-drag-type2+json': MyItem2;
}
}
A context provider for use-dnd
. All use-dnd
hooks must be within the context (i.e. descendents of DragDropProvider
). You should only need one DragDropProvider
, likely near the root of your document.
DragDropProvider
does not currently support setting custom element roots other than document
. Fortunately, due to the way use-dnd
works, custom roots are less likely to be necessary than in react-dnd
.
Make an element draggable with the useDrag
hook. You can supply options using either a function and a dependency array or a plain object.
const [collected, drag, dragPreview] = useDrag(() => {
type: 'application/my-drag-type+json',
item: obj,
}, [obj]);
Make an element a drag handle by using its ref
prop:
<div ref={drag}>Drag me!</div>
If you want the drag handle and the drag preview to be separate, use the drag preview similarly:
<div ref={dragPreview}><DragIcon ref={drag}/> Drag me!</div>
Type: string
The drag type of the item. use-dnd
does not currently support using multiple drag types.
Type: ItemContent<...>
The item that is being dragged.
Type: (item: ItemContent<...>) => string
Default: See below
A function to serialize the item into a string. By default, the identity if the type starts with text/
and JSON.stringify
otherwise. This is necessary if the item is not automatically serializable and you want it to be able to be picked up by foreign drop targets.
Type: (event: DragEvent) => void
Default: undefined
A callback which is fired when the item is initially picked up. This is the only time that event.dataTransfer.effectAllowed
can be set.
Type: (event: DragEvent) => void
Default: undefined
A callback which is fired when the item is no longer being dragged.
Type: (event?: DragEvent) => void
Default: undefined
A callback which is fired whenever an update occurs. If the item is not being dragged, event
will be undefined
. The first item in the hook result array will be the value returned by collect
.
Make an element a drop target with the useDrop
hook. You can supply options using either a function and a dependency array or a plain object.
const [{ isDragging }, drop] = useDrop(() => ({
type: 'application/my-drag-type+json',
collect: data => ({
isDragging: Boolean(data.itemType),
}),
drop(data) {
loadItem(data);
}
}), [loadItem]);
Type: string | readonly string[] | null
The drag type(s) which can be dropped on the target. Use null
to accept all drag types.
Type: (info: DropStatus) => Collected
Default: undefined
A callback which is fired whenever an update occurs. If an accepted item is not being dragged, info
will be {}
. The first item in the hook result array will be the value returned by collect
.
Foreign objects are indicated by info.item
being undefined
.
Type: (info: EventStatus) => void
Default: undefined
A callback which is fired when an item is dropped on the target.
Type: (info: EventStatus) => void
Default: undefined
A callback which is fired when an item is hovered over the target.
Foreign objects are indicated by info.item
being undefined
.
Type: <ItemType extends ...>(type: ItemType, data: string) => ItemContent<ItemType>
Default: See below
A function to deserialize the item from a string. By default, the identity if the item type starts with text/
and JSON.parse
otherwise. This is necessary if the item is not automatically deserializable and you want to be able to pick up foreign objects.
Type: boolean
Default: false
If set to true
, foreign items will be accepted. A foreign item is an item which is created by a drag source outside of the root, such as by another window or application. If set to false
, foreign items will be treated the same as if they had a non-accepted item type.
Observe the currently dragged element with useDragLayer
. Acts like useDrop
but with no drop target and only the type
and collect
option.
const { isDragging, content, x, y } = useDragLayer(
'application/my-drag-type+json',
info => ({
isDragging: Boolean(info.itemType),
content: info.item,
x: info.event?.clientX,
y: info.event?.clientY,
})
);
useDragLayer
is useful for providing custom drag preview behavior.
Type: string | readonly string[] | null
The drag type(s) which can be dropped on the target. Use null
to accept all drag types.
Type: (info: DragLayerStatus) => Collected
A callback which is fired whenever an update occurs. If an accepted item is not being dragged, info
will be {}
. The hook result will be the value returned by collect
.
Foreign objects are indicated by info.item
being undefined
.
This can be used in conjunction with the dragPreview
from useDrag
to hide the drag preview. This may be useful if you want to provide your own custom drag overlay, such as with useDragLayer
.
const [, drag, dragPreview] = useDrag(...);
useEffect(() => {
dragPreview(createEmptyPreviewImage());
}, [dragPreview]);