Higher-order components to turn any list into animated, accessible and touch-friendly sortable lists
—
Higher-order component that makes individual React components sortable within a SortableContainer. Provides simple interface for element positioning, grouping, and state management.
Makes any React component sortable within a SortableContainer.
/**
* Higher-order component that makes an element sortable within a SortableContainer
* @param wrappedComponent - The React component to enhance with sortable functionality
* @param config - Optional configuration object
* @returns Enhanced React component with sortable element capabilities
*/
function SortableElement<P>(
wrappedComponent: WrappedComponent<P>,
config?: Config
): React.ComponentClass<P & SortableElementProps>;
interface SortableElementProps {
/** Element's sort index within its collection (required) */
index: number;
/** Collection identifier for grouping elements */
collection?: Offset;
/** Whether the element should be sortable */
disabled?: boolean;
}
interface Config {
withRef: boolean;
}Usage Examples:
import React from 'react';
import { SortableElement } from 'react-sortable-hoc';
// Basic sortable item
const SortableItem = SortableElement(({ value }) => <li>{value}</li>);
// More complex sortable component
const SortableCard = SortableElement(({ title, description, onEdit }) => (
<div className="card">
<h3>{title}</h3>
<p>{description}</p>
<button onClick={onEdit}>Edit</button>
</div>
));
// Usage in container
const items = ['Item 1', 'Item 2', 'Item 3'];
{items.map((value, index) => (
<SortableItem
key={`item-${index}`}
index={index}
value={value}
/>
))}/** Element's sortable index within its collection (required) */
index: number;The index prop is required and determines the element's position in the sortable sequence. It must be unique within the collection.
Index Examples:
// Basic sequential indexing
{items.map((item, index) => (
<SortableItem key={item.id} index={index} value={item.value} />
))}
// Custom indexing for sparse arrays
const sparseItems = [
{ id: 'a', value: 'First', sortOrder: 0 },
{ id: 'b', value: 'Second', sortOrder: 2 },
{ id: 'c', value: 'Third', sortOrder: 5 },
];
{sparseItems.map((item) => (
<SortableItem
key={item.id}
index={item.sortOrder}
value={item.value}
/>
))}/** Collection identifier for grouping elements */
collection?: number | string; // default: 0The collection prop groups elements into separate sortable areas within the same container. Useful for multi-list scenarios or categorized sorting.
Collection Examples:
// Multiple lists in same container
const todoItems = [
{ id: 1, text: 'Task 1', status: 'pending' },
{ id: 2, text: 'Task 2', status: 'completed' },
{ id: 3, text: 'Task 3', status: 'pending' },
];
{todoItems.map((item, index) => (
<SortableTask
key={item.id}
index={index}
collection={item.status} // Group by status
task={item}
/>
))}
// Numeric collections
{leftColumnItems.map((item, index) => (
<SortableItem key={item.id} index={index} collection={0} value={item} />
))}
{rightColumnItems.map((item, index) => (
<SortableItem key={item.id} index={index} collection={1} value={item} />
))}/** Whether the element should be sortable */
disabled?: boolean; // default: falseWhen disabled is true, the element cannot be dragged or participate in sorting operations.
Disabled Examples:
// Conditionally disable elements
{items.map((item, index) => (
<SortableItem
key={item.id}
index={index}
value={item.value}
disabled={item.locked || item.readonly}
/>
))}
// Disable based on user permissions
{items.map((item, index) => (
<SortableItem
key={item.id}
index={index}
value={item.value}
disabled={!userCanSort}
/>
))}All props except index, collection, and disabled are passed through to the wrapped component.
const SortableProductCard = SortableElement(({
product,
onAddToCart,
onViewDetails,
className
}) => (
<div className={`product-card ${className}`}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => onAddToCart(product)}>Add to Cart</button>
<button onClick={() => onViewDetails(product)}>View Details</button>
</div>
));
// Usage with additional props
<SortableProductCard
key={product.id}
index={index}
product={product}
className="featured"
onAddToCart={handleAddToCart}
onViewDetails={handleViewDetails}
/>To access sortable-specific props in your component, pass them with different names:
const SortableListItem = SortableElement(({
value,
sortIndex, // Renamed from index
isDisabled // Renamed from disabled
}) => (
<li className={isDisabled ? 'disabled' : ''}>
{value} - Position: {sortIndex}
</li>
));
// Usage
<SortableListItem
key={item.id}
index={index}
sortIndex={index} // Pass index again with different name
disabled={item.locked}
isDisabled={item.locked} // Pass disabled again with different name
value={item.value}
/>Sortable elements can contain interactive elements, but event handling requires careful consideration:
const InteractiveSortableItem = SortableElement(({
item,
onEdit,
onDelete
}) => (
<div className="sortable-item">
<span>{item.title}</span>
<div className="actions">
<button
onClick={(e) => {
e.preventDefault(); // Prevent drag
onEdit(item);
}}
>
Edit
</button>
<button
onClick={(e) => {
e.preventDefault(); // Prevent drag
onDelete(item);
}}
>
Delete
</button>
</div>
</div>
));interface Config {
/** Enable access to wrapped component instance */
withRef: boolean;
}withRef Example:
const SortableItem = SortableElement(ItemComponent, { withRef: true });
// Access wrapped instance
const itemRef = useRef();
const wrappedInstance = itemRef.current?.getWrappedInstance();
<SortableItem
ref={itemRef}
index={index}
value={value}
/>const [items, setItems] = useState([
{ id: 1, text: 'Item 1', category: 'A' },
{ id: 2, text: 'Item 2', category: 'B' },
{ id: 3, text: 'Item 3', category: 'A' },
]);
const handleSortEnd = ({ oldIndex, newIndex, collection }) => {
// Sort within specific collection
const categoryItems = items.filter(item => item.category === collection);
const otherItems = items.filter(item => item.category !== collection);
const sortedCategoryItems = arrayMove(categoryItems, oldIndex, newIndex);
setItems([...otherItems, ...sortedCategoryItems]);
};
{items.map((item, index) => (
<SortableItem
key={item.id}
index={items.filter(i => i.category === item.category).indexOf(item)}
collection={item.category}
value={item.text}
/>
))}const ConditionalSortableItem = ({ item, index, showDetails }) => {
const ItemComponent = ({ value, details }) => (
<div className="item">
<span>{value}</span>
{showDetails && <div className="details">{details}</div>}
</div>
);
const SortableItemComponent = SortableElement(ItemComponent);
return (
<SortableItemComponent
index={index}
value={item.title}
details={item.description}
/>
);
};Install with Tessl CLI
npx tessl i tessl/npm-react-sortable-hoc