From React Sortable HOC to dnd kit

Alena Khineika
Nerd For Tech
Published in
9 min readJan 15, 2023

--

How to move from react-sortable-hoc and React 16 to dnd-kit and React 17.

Good news! You don’t have to do the whole refactoring at once. If you just bump the React version, having react-sortable-hoc won’t break your application. But you have to replace react-sortable-hoc sooner or later, because the library is no longer actively maintained and uses the findDOMNode method under the hood, which will be deprecated in future versions of React. This method is a critical piece of the react-sortable-hoc architecture, and the library will stop working when that method is removed from react-dom.

The upgrade to React 17 on its own is pretty straightforward:

  • Bump the react and react-dom dependencies to v17.
  • Update other dependencies to versions that work with React 17. For example, replace the enzyme-adapter-react-16 package with @wojtekmaj/enzyme-adapter-react-17.
  • Enable StrictMode rendering for highlighting potential problems in the application, and resolve warnings that start showing in the console.

You can enable strict mode for any part of your application. For example:

ReactDOM.render(
<React.StrictMode><App /></React.StrictMode>,
document.getElementById('root')
);

When a strict mode is enabled, React starts complaining about deprecated or unsafe API usage. For instance, you will start seeing in the console warnings about legacy string refs or deprecated findDOMNode usage. They are both likely to be removed in one of the future releases, therefore they should be on their way out.

A legacy string ref looks something like this:

const el = ReactDOM.findDOMNode(this.refs.container);
<div ref="container" />

And this is an example of a callback ref:

<div ref={(container) => {
this.containerRef = container;
}} />

It receives a DOM element at mount time, so you can drop ReactDOM.findDOMNode usage.

Next, we replace react-sortable-hoc with dnd-kit that doesn’t use the deprecated findDOMNode API and is actually recommended by authors of react-sortable-hoc.

You can start by reading official documentation or comparing dnd-kit storybook with react-sortable-hoc storybook if you are curious.

In this article, I want to share a comparative transition from react-sortable-hoc to dnd-kit, something that I was doing for one of the projects.

The dnd toolkit provides feature parity with react-sortable-hoc, but offers a different API to handle events and specify draggable elements and droppable areas.

react-sortable-hoc

Let’s imagine that we already have the following application that works with react-sortable-hoc:

import React, { useRef, useState } from 'react';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { arrayMoveImmutable } from 'array-move';
import type { SortableItem, SortableListProps, SortProps, SortableComponentProps } from './../../types';

const SortableItem = SortableElement(({ id }: SortableItem) => (
<li className="sortable-item">
<span>Item {id}</span>
</li>
));

const SortableList = SortableContainer(({ items }: SortableListProps) => {
return (
<ul className="sortable-list">
{items.map((id, index) => (
<SortableItem key={`item-${id}`} id={id} index={index} />
))}
</ul>
);
});

export const ReactSortableHocList: React.FunctionComponent<SortableComponentProps> = ({}) => {
const [items, setItems] = useState<number[]>([0, 1, 2, 3, 4, 5]);
let containerRef = useRef<HTMLDivElement | null>(null);

const onSortEnd = ({ oldIndex, newIndex }: SortProps) => {
setItems((items) => arrayMoveImmutable(items, oldIndex, newIndex));
};

return (
<div ref={(ref) => { containerRef.current = ref; }}>
<SortableList
items={items}
onSortEnd={onSortEnd}
transitionDuration={0}
helperContainer={() => (containerRef.current ?? document.body)}
helperClass="dragging-react-sortable-hoc"
distance={10}
/>
</div>
);
}

The SortableList component is built with a SortableContainer wrapper to declare its area suitable for draggable and sortable components. SortableContainer has a number of props, but we are interested in these two:

  • items —An initial items list that we want dragging/sorting.
  • onSortEnd — A callback function that is being invoked when dragging/sorting ends.

SortableList iterates through the list of initial items and for each item it creates a SortableItem component. SortableItem is built with a SortableElement wrapper to make the item draggable and sortable.

In the previous example, the whole li element is draggable, but many applications expose the concept of “drag handles”. They wrap the handle with the SortableHandle HOC and place it inside SortableElement.

import { SortableHandle } from 'react-sortable-hoc';
import { DraggingHandle } from './../button/dragging-handle';

export const DraggableHandle = SortableHandle(() => (<DraggingHandle />));

const SortableItem = SortableElement(({ id }: SortableItem) => (
<li className="sortable-item">
<span>Item {id}</span>
<div className="dragging-handle-container">
<DraggableHandle />
</div>
</li>
));

Applications that use the SortableHandle HOC set also the useDragHandle of SortableContainer to true.

In the left list, the whole li element is draggable, while in the right list only grabbing a handle sorts the items.

I created a sample project on GitHub so you can see the full code and try the working app ✨

But how to move it towards dnd-kit?

dnd-kit

Here is a rough mapping of react-sortable-hoc to dnd-kit API. This mapping does not consider the architecture of the libraries, it is more like use-case mapping to make it easier to refactor the existing codebase.

  • SortableContainer -> DndContext + SortableContext
  • SortableElement / SortableHandle -> useSortable

As you can tell this is not a 1-to-1 mapping :/

If you are eager to jump to the dnd-kit code snippet, here it is:

import React, { useCallback, useState } from 'react';
import { arrayMoveImmutable } from 'array-move';
import { DndContext, MouseSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import type { UniqueIdentifier } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS as cssDndKit } from '@dnd-kit/utilities';
import type { SortableItemProps, SortableListProps, SortableComponentProps } from './../../types';

function SortableItem({ id, activeId }: SortableItemProps) {
const { setNodeRef, transform, transition, listeners } = useSortable({ id });
const style = {
transform: cssDndKit.Transform.toString(transform),
transition,
};

return (
<li
ref={setNodeRef}
style={style}
{...listeners}
className={(activeId === id) ? 'sortable-item dragging-dbd-kit' : 'sortable-item'}
>
<span>Item {id - 1}</span>
</li>
);
}

const SortableList = ({ items, onSortEnd }: SortableListProps) => {
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
const getIndex = (id: UniqueIdentifier) => items.indexOf(+id);
const sensors = useSensors(
useSensor(MouseSensor, {
// Require the mouse to move by 10 pixels before activating.
// Slight distance prevents sortable logic messing with
// interactive elements in the handler toolbar component.
activationConstraint: {
distance: 10,
},
}),
useSensor(TouchSensor, {
// Press delay of 250ms, with tolerance of 5px of movement.
activationConstraint: {
delay: 250,
tolerance: 5,
},
})
);

return (
<DndContext
sensors={sensors}
autoScroll={false}
onDragStart={({ active }) => {
if (active) {
setActiveId(active.id);
}
}}
onDragEnd={({ active, over }) => {
if (over && active.id !== over.id) {
onSortEnd({
oldIndex: getIndex(active.id),
newIndex: getIndex(over.id),
});
}
setActiveId(null);
}}
onDragCancel={() => setActiveId(null)}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<ul className="sortable-list">
{items.map((id, index) => (
<SortableItem key={`item-${id}`} id={id} activeId={activeId} />
))}
</ul>
</SortableContext>
</DndContext>
);
}

export const DndKitList: React.FunctionComponent<SortableComponentProps> = () => {
// The SortableContext unique identifiers
// must be strings or numbers bigger than 0.
const [items, setItems] = useState<number[]>(
() => [0, 1, 2, 3, 4, 5].map((id) => id + 1)
);

const onSortEnd = useCallback(
({ oldIndex, newIndex }) => {
setItems((items) => arrayMoveImmutable(items, oldIndex, newIndex));
},
[items]
);

return (<SortableList items={items} onSortEnd={onSortEnd} />);
}

The SortableContainer is replaced with the DndContext component, which allows your draggable/sortable components to interact with each other and uses React Context API to share data between the components and hooks. Let’s see what else has changed:

  • The sensors are added. They are an abstraction to manage and listen to different input methods and may define one or multiple activator events.
  • The items property is moved from the outer DndContext wrapper to a nested SortableContext wrapper.

In addition to the DndContext provider, dnd-kit requires the SortableContext provider. The initial items become children of the SortableContext component, but we also pass items’ IDs separately via the internal items property of SortableContext.

It’s important that the items prop passed to SortableContext is sorted in the same order in which the items are rendered, otherwise you may see unexpected results.

So now we have two context providers and no sortable element wrappers! The SortableElement is gone and there is no dnd-kitequivalent. But how then dnd-kit knows which components should be sortable?

<SortableContext items={items} strategy={verticalListSortingStrategy}>
<ul className="sortable-list">
{items.map((id, index) => (
<DragHandleToolbar key={`item-${id}`} id={id} activeId={activeId} />
))}
</ul>
</SortableContext>

Thanks to the items property, the SortableContext provider contains the sorted array of the unique identifiers associated with each sortable item. We pass down the unique id to each nested element and use the useSortable hook to set up DOM nodes as draggable/sortable areas based on the received id.

function SortableItem({ id, activeId }: SortableItemProps) {
const { setNodeRef, transform, transition, listeners } = useSortable({ id });
const style = {
transform: cssDndKit.Transform.toString(transform),
transition,
};

return (
<li
ref={setNodeRef}
style={style}
{...listeners}
className={(activeId === id) ? 'sortable-item dragging-dbd-kit' : 'sortable-item'}
>
<span>Item {id - 1}</span>
</li>
);
}

You need to pass setNodeRef that is returned by the useSortable hook to a DOM element so that it can register the underlying DOM node and keep track of it to detect collisions and intersections with other draggable elements.

The useSortable hook requires that you attach listeners to the DOM node that you would like to become the activator to start dragging.

In order to actually see your draggable items move on the screen, you’ll need to move them using CSS. You can use inline styles, CSS variables, or even CSS-in-JS libraries to pass CSS to the style property of the draggable element.

Similarly to react-sortable-hoc you can introduce a handle component to make only this small area responsible for dragging and sorting.

import { DraggingHandle } from './../button/dragging-handle';

export const DraggableHandle = ({ handleListeners }: DraggableHandleProps) => {
return (<div {...handleListeners}><DraggingHandle /></div>);
};

function SortableItem({ id, activeId, hasDraggHandle }: SortableItemProps) {
const { setNodeRef, transform, transition, listeners } = useSortable({ id });
const style = {
transform: cssDndKit.Transform.toString(transform),
transition,
};

return (
<li
ref={setNodeRef}
style={style}
{...(hasDraggHandle ? {} : listeners)}
className={(activeId === id) ? 'sortable-item dragging-dbd-kit' : 'sortable-item'}
>
<span>Item {id - 1}</span>
<div className="dragging-handle-container">
{hasDraggHandle ? <DraggableHandle handleListeners={listeners}/> : null}
</div>
</li>
);
}

The difference with react-sortable-hoc is that dnd-kit does not provide a wrapper for the sortable handle. You need to assign setNodeRef from the useSortable hook to the DOM element you intend on turning into a draggable area and pass listeners to a different DOM element that you would like to activate dragging.

My electron-sortable-list app renders two lists side by side so that you can compare their behaviors and visual effects.

Toggle the “Use Dragging Handle” to grab the whole item or use the handle to sort items in the list.

Alternatives

There are a couple of notable alternatives to dnd-kit.

One of them is called react-beautiful-dnd and is built specifically for draggable lists (vertical, horizontal, movement between lists, nested lists, and so on). However, it does not provide the breadth of functionality offered for example by react-dnd, that allows dragging between different parts of the application. It uses the HTML5 drag-and-drop API which is the only way to handle the file drop events. So if your application requires to handle file drag and drop functionality, react-dnd would be the right choice. The downside of this library is the learning curve. It is easy to get started with the library, but it can be very tricky to add complex customizations.

There are lots of the drag and drop libraries out there, and all of them have their own use cases. To make a choice read the library documentation, try storybooks to get the feel of usage, and check package download counts over time.

Make sure that the library is not abandoned by creators and that its dependencies are up-to-date. The usage of react-sortable-hoc or e.g. react-draggable won’t break your application for now. But they both rely on the ReactDOM.findDOMNode API and it is just a matter of time before you will have to replace them.

I hope this article was interesting reading for you and maybe helps someone to adopt dnd-kit by their application!

--

--