React DnDを使ってみた
使うファイル
index.js
App.js
Item.js
公式サイト
https://react-dnd.github.io/react-dnd/about
参考動画
https://www.youtube.com/watch?v=aK2PD_REk7A&list=PLmiHocWADqPGYeEqgwl5w9BKCi5ju3_Xj&index=10&t=1s
まずはインストールから
npm install react-dnd react-dnd-html5-backend
これで準備完了
index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import * as serviceWorker from './serviceWorker'; import { DndProvider } from 'react-dnd' //追加 import { HTML5Backend } from 'react-dnd-html5-backend' //追加 ReactDOM.render( <React.StrictMode> <DndProvider backend={HTML5Backend}> //この書き方はテンプレなので覚えなくていい <App /> </DndProvider> </React.StrictMode>, document.getElementById('root') ); serviceWorker.unregister();
react-dndを使いたい所を
react-routerを使う時と同じ感じ。
App.js
import React, { useState } from 'react'; import Item from './Item'; import { useDrag, useDrop } from "react-dnd"; import { ItemTypes } from './Item'; function App() { const [list, setList] = useState( [ { id: 1, name: "aaa" }, { id: 2, name: "bbb" }, { id: 3, name: "ccc" }, { id: 4, name: "ddd" } ] ) function moveItem(dragIndex, dropIndex) { const item = list[dragIndex] setList(prevState => { const newitem = prevState.filter((i, index) => index !== dragIndex) newitem.splice(dropIndex, 0, item) return [...newitem] }) } const [{ isOver }, drop] = useDrop({ accept: ItemTypes, collect: monitor => ({ isOver: monitor.isOver() }) }); const style = { backgroundColor: isOver ? '#00bfff' : '', listStyle:"none", padding:0 } return ( <div className="App"> <ul style={style} ref={drop}> {list.map((item, index) => { return ( <Item key={item.id} item={item} index={index} moveItem={moveItem}/> ) })} </ul> </div> ); } export default App;
ここでは、ドロップ可能な範囲に掴んでいるアイテムがあるときにその範囲を青く光らせています。
それをしているのが、isOver: monitor.isOver()です。
公式サイトによると、true進行中のドラッグ操作があり、現在ポインターがオーナーの上に置かれている場合に返されます。
https://react-dnd.github.io/react-dnd/docs/api/drop-target-monitor
つまり、掴んでいるアイテムがあり、それがrefで指定している要素の上にあればtrueになるということだと思います。
itemにindexを渡した方があとで探す手間が省けるのであったほうがよい。
Item.js
import React, { useRef } from "react"; import { useDrag, useDrop } from "react-dnd"; export const ItemTypes = "ITEM"; //必ず指定する function Item({ item, index, moveItem, onDrop }) { const ref = useRef(null); const [{ isOver }, drop] = useDrop({ accept: ItemTypes, hover: item => { const dragIndex = item.index; const dropIndex = index; moveItem(dragIndex, dropIndex); //ここの2つはなくてもいいが、あったほうが動きがわかりやすい item.index = dropIndex; }, drop: item => { const dragIndex = item.index; const dropIndex = index; moveItem(dragIndex, dropIndex); }, collect: monitor => ({ isOver: monitor.isOver() }) }) const [{ isDragging }, drag] = useDrag({ item: { type: ItemTypes, ...item, index }, collect: monitor => ({ isDragging: monitor.isDragging() }) }); const liststyle = { cursor: 'move', margin: "20px 0px", height: 100, lineHeight: "100px", border: "solid 1px black" } const style = { backgroundColor: 'white', opacity: isDragging ? 0 : 1 } drag(drop(ref)); //どちらも同じrefを使うため return ( <div ref={ref} style={style} > <li style={liststyle}> {item.name} </li> </div> ) } export default Item;
useDropのhoverでもmoveItemを使う場合item.index = dropIndexをしないと挙動があかしくなります。
おそらく、moveItemをしてlistを入れ替えたのに、掴んでいるアイテムのindexはかわらないため、indexの番号が一致しないためだと思われます。
useDragのitemで指定した物がuseDropでも使えるようになるので、自分なりにカスタマイズしてください。
userDragのisDragging()はドラッグしていればtrueになるので、 opacity: isDragging ? 0 : 1で掴んだアイテムが透明になります。
これはisOverでもできるが、isOverの場合はアイテムの上からマウスカーソルが離れてしまったら透明ではなくなるため、今回はisDraggingを使っている。
意味があまり分からないと思うので、実際にopacity: isOver ? 0 : 1に変えてみて、liststyleのmarginをさらに大きくしてみれば書いてあることがよくわかると思う。
完成したもの