Edited at

Material-UI で並べ替え可能なリストを作る

Material-UI では、Material Design Guidelines で定められているドラッグ&ドロップで並べ替えられるリストが提供されていません。ドラッグ&ドロップの機能を提供するライブラリと組み合わせることで、これを実現できます。


ドラッグ&ドロップ

React でドラッグ&ドロップを実現するライブラリは数多ありますが、今回は以下の4つを比較・検討しました。箇条書きはあくまで個人的な感想です。


react-dnd

react-dnd/react-dnd: Drag and Drop for React


  • 一番有名どころ。オリジナルの作者は、React や Redux の Dan Abramov (gaearon) 氏。

  • 拡張性が高いので、その気になればいろんなことができそう。一方で、アニメーションなど自力で実装しなければならない部分が多い。

  • 記述量は多め。


react-beautiful-dnd

atlassian/react-beautiful-dnd: Beautiful and accessible drag and drop for lists with React


  • Atlassian 製。

  • ソートのアニメーションが優れている。記述量は多め。

  • “Not for everyone” だそうだ。たとえば、今回必要な「軸の固定 (axis locking) 」は、何度か機能提案されているが、UX を損なう (意訳) としてすべて却下されている。

  • ということで、“Not for me” だった。


react-sortable-hoc

clauderic/react-sortable-hoc: A set of higher-order components to turn any list into an animated, touch-friendly, sortable list ✌️


  • 記述量が少なくて済むので、実装が楽。

  • 現在非推奨な findDOMNode が使われていてレガシーな感じ。

  • Material-UI で DragHandle をやろうとするとちょっとハマった (後述)。


react-smooth-dnd

kutlugsahin/react-smooth-dnd: react wrapper components for smooth-dnd


  • Star の数では劣るが、特に問題はなさそう。

  • 記述量は少なくて済む。ハマりポイントもなく、一番素直に実装できた。

  • タッチデバイスだと難ありらしい (Issue が上がっている)。


実装例

react-sortable-hoc で実装してみた例と、react-smooth-dnd で実装してみた例を記載します。(手元で実装したものと CodeSandbox に上げたもので、使用したパッケージのバージョンが異なりますが、動作に問題はありません。)


react-sortable-hoc

Edit Material-UI Sortable List with react-sortable-hoc


使用したパッケージ


  • React v16.8.5

  • Material-UI v3.9.2

  • react-sortable-hoc v1.8.3

  • array-move v2.0.0

import React, { useState } from 'react';

import ReactDOM from 'react-dom';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';

const DragHandle = SortableHandle(() => (
<ListItemIcon>
<DragHandleIcon />
</ListItemIcon>
));

const SortableItem = SortableElement(({ text }) => (
<ListItem ContainerComponent="div">
<ListItemText primary={text} />
<ListItemSecondaryAction>
<DragHandle />
</ListItemSecondaryAction>
</ListItem>
));

const SortableListContainer = SortableContainer(({ items }) => (
<List component="div">
{items.map(({ id, text }, index) => (
<SortableItem key={id} index={index} text={text} />
))}
</List>
));

const SortableList = () => {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
{ id: '4', text: 'Item 4' }
]);

const onSortEnd = ({ oldIndex, newIndex }) => {
setItems(items => arrayMove(items, oldIndex, newIndex));
};

return (
<SortableListContainer
items={items}
onSortEnd={onSortEnd}
useDragHandle={true}
lockAxis="y"
/>
);
};

ReactDOM.render(<SortableList />, document.getElementById('root'));

ハマった点ですが、ドラッグ中の li 要素が document.body にクローンされるため、リストのマーカーが表示されてしまって、表示崩れを起こしてしまいました。これを回避するにはいくつかの方法があります。



  • bodylist-style-type: none; を適用する。


  • ListItemSecondaryActionSortableHandle に含めてしまう。


  • ListItemContainerComponent="div" を指定する。

実装例では3番目の方法を採用しています。ListItem だけ変更すると、実際の DOM で ul の直下に div がきて気持ち悪いので、List にも component="div" を指定しています。


react-smooth-dnd

Edit Material-UI Sortable List with react-smooth-dnd


使用したパッケージ


  • React v16.8.5

  • Material-UI v3.9.2

  • react-smooth-dnd v0.8.2

  • array-move v2.0.0

import React, { useState } from 'react';

import ReactDOM from 'react-dom';
import { Container, Draggable } from 'react-smooth-dnd';
import arrayMove from 'array-move';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import DragHandleIcon from '@material-ui/icons/DragHandle';

const SortableList = () => {
const [items, setItems] = useState([
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
{ id: '4', text: 'Item 4' }
]);

const onDrop = ({ removedIndex, addedIndex }) => {
setItems(items => arrayMove(items, removedIndex, addedIndex))
};

return (
<List>
<Container dragHandleSelector=".drag-handle" lockAxis="y" onDrop={onDrop}>
{items.map(({ id, text }) => (
<Draggable key={id}>
<ListItem>
<ListItemText primary={text} />
<ListItemSecondaryAction>
<ListItemIcon className="drag-handle">
<DragHandleIcon />
</ListItemIcon>
</ListItemSecondaryAction>
</ListItem>
</Draggable>
))}
</Container>
</List>
);
};

ReactDOM.render(<SortableList />, document.getElementById('root'));

先述の通り、現バージョンではタッチデバイスで難ありらしいですが、PC 向けにはこれで問題ないように思います。


余談

いずれ React Hooks を使って useDragAndDrop みたいに書けるようになるかもしれないですね。実装が楽になって、見通しがよくなるといいですね。