Skip to main content

Cross-Container Drag Experimental

caution

Cross-container drag is experimental. The API may change in future versions.

Move items between sortable lists — perfect for multi-column layouts, cross-list reorder, and categorization interfaces.

Architecture

Cross-container drag uses three additional primitives:

PrimitiveRole
useSortableBoardBoard-level coordinator for cross-container transfers
SortableBoardContainerMonitoring wrapper providing board context
SortableBoardContextAuto-registration context for column containers

Each column independently uses useSortableList + SortableContainer + SortableItem.

Basic Pattern

import {
DraxProvider,
useSortableBoard,
SortableBoardContainer,
useSortableList,
SortableContainer,
SortableItem,
} from 'react-native-drax';

function CrossListBoard() {
const [columns, setColumns] = useState(initialColumns);

const board = useSortableBoard({
keyExtractor: (item) => item.id,
onTransfer: ({ item, fromContainerId, toContainerId, fromIndex, toIndex }) => {
setColumns((prev) => {
const next = { ...prev };
// Remove from source
next[fromContainerId] = prev[fromContainerId].filter((_, i) => i !== fromIndex);
// Insert into target
next[toContainerId] = [
...prev[toContainerId].slice(0, toIndex),
item,
...prev[toContainerId].slice(toIndex),
];
return next;
});
},
});

return (
<DraxProvider>
<SortableBoardContainer board={board} style={styles.board}>
{Object.entries(columns).map(([colId, items]) => (
<Column key={colId} id={colId} items={items} board={board} />
))}
</SortableBoardContainer>
</DraxProvider>
);
}

Column Component

Each column is a standard sortable list:

function Column({ id, items, board }) {
const [data, setData] = useState(items);
const listRef = useRef<FlatList>(null);

const sortable = useSortableList({
id, // Must match the column key
data,
keyExtractor: (item) => item.id,
onReorder: ({ data }) => setData(data),
});

// Sync external data changes (from transfers)
useEffect(() => { setData(items); }, [items]);

return (
<SortableContainer sortable={sortable} scrollRef={listRef}>
<FlatList
ref={listRef}
data={sortable.data}
keyExtractor={sortable.stableKeyExtractor}
onScroll={sortable.onScroll}
onContentSizeChange={sortable.onContentSizeChange}
renderItem={({ item, index }) => (
<SortableItem sortable={sortable} index={index}>
<Card item={item} />
</SortableItem>
)}
/>
</SortableContainer>
);
}

How It Works

  1. Drag startsSortableBoardContainer records source column and item info
  2. Crosses boundary — When the hover enters a different column:
    • Source column ejects the item (items close the gap)
    • Target column creates a phantom slot (items make room)
  3. Position updates — Phantom slot moves as finger moves within target column
  4. DroponTransfer fires with source/target info, hover snaps to phantom position
  5. Cleanup — Both columns re-render with updated data, hover fades out

Transfer Event

onTransfer: ({ item, fromContainerId, fromIndex, toContainerId, toIndex }) => {
// item: the transferred item
// fromContainerId: source column id
// fromIndex: original index in source
// toContainerId: target column id
// toIndex: insertion index in target
}

Next Steps