Skip to content

Conversation

@clauderic
Copy link
Owner

This PR introduces a useDragHandle hook that allows React consumers to set a drag handle in a different location than where useDraggable or useSortable is used.

Resolves #1738

Example usage — two separate components

Below the draggable is declared in a row component, while the dedicated handle lives in a different component.

import {DragDropProvider} from '@dnd-kit/react';
import {useSortable} from '@dnd-kit/react/sortable';
import {useDragHandle} from '@dnd-kit/react';

import {
  ColumnDef,
  Row,
  flexRender,
  useReactTable,
  getCoreRowModel,
} from '@tanstack/react-table';

// ─────────────────────────────────────────────────────
// Cell renderer — only cares about being the drag handle
// ─────────────────────────────────────────────────────
function DragCell({rowId}: {rowId: string}) {
  const handleRef = useDragHandle({id: rowId});   // same id as the row
  return (
    <button ref={handleRef} aria-label="Drag row" style={{cursor: 'grab'}}></button>
  );
}

// ─────────────────────────────────────────────────────
// Draggable row — owns the sortable behaviour
// ─────────────────────────────────────────────────────
function DraggableRow({row}: {row: Row<Person>}) {
  const {ref} = useSortable({id: row.id, index: row.index});        // row itself is sortable
  return (
    <tr ref={ref}>
      {row.getVisibleCells().map((cell) => (
        <td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
      ))}
    </tr>
  );
}

// ─────────────────────────────────────────────────────
// Table setup (irrelevant plumbing omitted for clarity)
// ─────────────────────────────────────────────────────
function App() {
  const columns = React.useMemo<ColumnDef<Person>[]>(() => [
    {                                             // dedicated “move” column
      id: 'drag',
      header: '↕︎',
      size: 40,
      cell: ({row}) => <DragCell rowId={row.id} />, // ← handle rendered here
    },
    // …other columns…
  ], []);

  const table = useReactTable({
    data, columns, getCoreRowModel: getCoreRowModel(), getRowId: r => r.id,
  });

  return (
    <DragDropProvider>
      <table>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <DraggableRow key={row.id} row={row} />
          ))}
        </tbody>
      </table>
    </DragDropProvider>
  );
}

The key takeaway: useDragHandle does not need to live in the same component where useDraggable/useSortable is invoked—perfect for table rows, list items with nested actions, or any layout where the “grab-area” is rendered deeper in the tree.

@changeset-bot
Copy link

changeset-bot bot commented Jun 12, 2025

⚠️ No Changeset found

Latest commit: 5b90176

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jun 12, 2025

Open in StackBlitz

@dnd-kit/abstract

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/abstract@1740

@dnd-kit/collision

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/collision@1740

@dnd-kit/dom

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/dom@1740

@dnd-kit/geometry

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/geometry@1740

@dnd-kit/helpers

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/helpers@1740

@dnd-kit/react

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/react@1740

@dnd-kit/state

npm i https://pkg.pr.new/clauderic/dnd-kit/@dnd-kit/state@1740

commit: 5b90176

useOnElementChange(value, (handle) => {
if (!draggableInstance) return;

draggableInstance.handle = handle;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line does get flagged by the react compiler lint rule, but I can see this is using a ref inside a custom signals implementation so no idea if that the compiler is even a viable target for the library.

Mutating a value returned from a function whose return value should not be mutated (eslint react-compiler/react-compiler)

draggableInstance.handle = handle;
});

return handleRef;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type here is RefObject<Element | null>, so you'll probably want to match that to the existing handleRef type of (element: Element | null) => void or at least allow a generic for the element type so it doesn't complain when being applied to something like a <button>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants