はじめに
このドキュメントは自分の備忘録的にこの分かりづらいReact DnDを読み解くために書いたものです。なので一部しか翻訳していませんが、ご了承ください。
Hooks APIを使っているのでHooks中心になっています。
概要
React DnD は今までの大抵のドラッグ&ドロップライブラリとは違います。もしあなたがこれを使ったことないのであれば、威圧的なものになるでしょう。しかし、この設計のコアコンセプトを少しかじったことがあれば理解できるでしょう。残りのドキュメントを読む前にこれらのコンセプトについて一読するのをおすすめします。
コンセプトの一部は Flux や Redux のアーキテクチャに似ています。
これは偶然ではなく、React DnDの中でReduxを使っているからです。
バックエンド
React DnD は HTML5 drag and drop APIの上に作られています。 それはドラッグされているDOMノードをスクリーンショットし、”ドラッグプレビュー”として枠の外で使うので至って合理的です。これはカーソルの動きのような描画を一切しなくて済む点で便利です。このAPIはファイルドロップのイベントを操作する唯一の方法でもあります。
残念ながら、HTML5 drag and drop APIには良くない側面もあります。タッチスクリーンでは動かず、IEでは他のブラウザに比べてカスタマイズ性が下がります。
これが *HTML5 drag and drop サポートを使うことはReact DnDでは接続可能な一つの方法。*としている理由です。必ずしも使う必要はありません。タッチイベントやマウスイベントや他のイベントに沿った違う方法を取ることができます。 このような接続可能な方法はReact DnDではバックエンドと呼びます。 HTML5 backend だけがライブラリとしてありますが、将来的には増えていく予定です。
バックエンドはReactのイベントシステムのように振る舞います。ブラウザの違いを抽象化し、ネイティブなDOMイベントを実行します
Reactのイベントシステムと似ているとはいえ、React DnDのバックエンドはReactやそのイベントシステムとの依存関係はありません。
すべてのバックエンドはDOMイベントをReact DnDが処理できる形のRedux actionに内部的に翻訳しています。
Items と Types
Flux (または Redux)のように、React DnDはデータを扱い、viewを正しいソースとして使いません。何かをスクリーンをまたいでドラッグしたとき、我々はそれをコンポーネントやドラッグされているDOMノードとは呼日ません。そのかわりに、ドラッグされているあるtypeのitemと呼びます。
Itemとはなにか。itemはドラックされているものを示すプレーンなJavaScriptのオブジェクトです。
例えば、カンバンアプリにおいて、カードをドラッグしたときはitemは{ cardId: 42 }
のようになり、チェスのゲームではピースをピックしたときitemは { fromCell: 'C5', piece: 'queen' }
のように見えます。ドラッグされているデータをプレーンなオブジェクトとして表現することで、お互いのコンポーネントを切り離したり、知らなくしたりできるのです。
typeとはなにか。typeはstring(またはsymbol ) であり、アプリケーションにおいて、itemのすべてのクラスをユニークに特定するためのものです。カンバンアプリにおいて、ドラッグできるカードはcard
というtypeで示され、これらのカードをまとめた、ドラッグできるリストは list
type として示される。チェスでは一つpiece
というtypeがあるだろう。
Typesはアプリケーションを成長させていく上でいろいろなものをドラッグできるようにさせていく際に便利になるだろう。 しかし、すべてのドロップターゲットが突然新しいitemに反応されてほしくはない。Typesはどのドラッグ元とドロップターゲットが対応しているのかを指定することができるおそらくアプリケーションではReduxのaction typesの列挙を持つように、type定数の列挙を持つだろう。
Monitors
ドラッグしている状態であってもなくても、typeやitemがあってもなくても、ドラッグアンドドロップはStatefulと切り離すことはできません。
この状態はどこかで保持されています。
React DnDはmonitor と呼ばれる内部のstateストレージに薄いラッパーを通してこの状態をコンポーネントに提供します。
ドラッグアンドドロップの状態を追う必要のあるコンポーネントのどれでも、monitorから関連するStateを回収するコレクト関数を定義することができます。
チェスの駒がドラッグされているときにセルをハイライトしたいとします。
その時のCell
コンポーネントのコレクト関数は以下のようになります。
function collect(monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
}
}
この例はReact DnDがhighlighted
とhover
という更新された値をすべてのCell
インスタンスに対してpropsとして渡すことを示しています。
Connectors
バックエンドがDOMのイベントをハンドリングしているが、そのコンポーネントはDOMを表現するためにReactを使っている場合、どのようにバックエンドはリッスンするDOMノードを知ることができるのか。
コネクタについてです。コネクタはrender
関数の中で予め定義されたロール(ドラッグ元、ドラッグプレビュー、ドラッグターゲット)の一つをDOMノードにアサインすることができる。
実際、コネクタは上で説明したコレクト関数の第一引数として渡されます。それではどのようにドロップターゲットを指定して使うことができるのかを見てみましょう。
function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget(),
}
}
コンポーネントのrender
メソッドの中で、monitorから得られる両方のデータにアクセス可能。この関数はコネクターから得られます。
render() {
const { highlighted, hovered, connectDropTarget } = this.props;
return connectDropTarget(
<div className={classSet({
'Cell': true,
'Cell--highlighted': highlighted,
'Cell--hovered': hovered
})}>
{this.props.children}
</div>
);
}
connectDropTarget
はコンポーネントのルートのDOMノードが正しいドロップターゲットかをReactDnDに問い合わせます。そして、そのhoverやdropイベントはバックエンドによって処理されるでしょう。内部的にはあなたが指定したReact elementに callback ref を付与することで動作しています。コネクタによって返された関数はメモ化されshouldComponentUpdate
による最適化で破壊されません。
Drag Sources and Drop Targets
これまでのところ、itemsとtypesによって示されるDOMとデータ、そしてmonitorsやconnectorsによって得られる関数とともに動作するバックエンドについて触れてきた。React DnDがコンポーネントに何のpropsを注入していくかを説明しよう
しかし、どのようにしてこれらのpropsがコンポーネントに実際に注入されるのかを設定するのか。どのようにドラッグ&ドロップのイベントの応答による副作用を実行するのか。React DnDの最重要抽象概念であるdrag元とdropターゲットについて見てみよう。これらはtypesとitemsと副作用そしてコレクト関数をコンポーネントにまとめます
ドラッグ可能なコンポーネントを作りたいときはいつでも、ドラッグ元の宣言があるコンポーネントでラップする必要があります。すべてのドラッグ元はあるtypeが登録されています。そしてコンポーネントのpropsからitemを提供するメソッドを実行する必要がある。ドラッグ&ドロップイベントをハンドリングするいくつかの他のメソッドをオプショナルに指定することもできます。 ドラッグ元の宣言は与えられたコンポーネントのコレクト関数も指定することができます。
ドロップターゲットはドラッグ元にとても似ています。唯一違う点は一つのドロップターゲットが複数のitem typesを一度に設定できる点とitemを提供する代わりにitemのhoverやdropをハンドリングする点です。
Higher-Order Components and Decorators
コンポーネントをどのようにラップするか。また、ラッピングが何を意味するのか。いままでHoCを触ったことがなければ、 この記事を読んでください。コンセプトと詳細について触れています。*HoCは単にReactのclassコンポーネントを受け、違うReactのclassコンポーネントを返す関数です。*ライブラリから提供されているラッピングコンポーネントはあなたのコンポーネントのrender
メソッドをレンダリングし、propsを流し込むだけでなく、便利な挙動を付け加えます。
React DnDのDragSource とDropTarget では,他のトップレベルのexportされた関数と同様にHoCとなっています。これらはドラッグ&ドロップの魔法をあなたのコンポーネントに吹きかけます。
これらを使う注意点としては、2つの関数アプリケーションが必要です。例えば、 DragSource でどのようにあなたのコンポーネントをラップするかの例です。
import { DragSource } from 'react-dnd'
class YourComponent {
/* … */
}
export default DragSource(/* … */)(YourComponent)
DragSource の最初の関数の呼び出しにあるパラメータの指定のあと、2つ目の関数が呼ばれ、最後にあなたのclassを通ることを注意してください。これは currying や partial application と呼ばれ、 この箱の外で動作するために decorator syntax が必要です。
import { DragSource } from 'react-dnd'
@DragSource(/* … */)
export default class YourComponent {
/* … */
}
このシンタックスを必ずしも使う必要はありません。使いたいのであれば
Babelを使ってトランスパイルしましょう。{ “stage”: 1 }
というのを.babelrc file に記述します。
もしデコレータを使わないのであれば、一部のアプリケーションではまだ使いやすいでしょう。というのもいくつかの DragSource と DropTargetの宣言をJavaScriptで_.flowのような関数構成ヘルパーをつかって合体させることができます。デコレータを使えば、同様の効果を得るためにデコレータを積み重ねる事ができます。
import { DragSource, DropTarget } from 'react-dnd'
import flow from 'lodash/flow'
class YourComponent {
render() {
const { connectDragSource, connectDropTarget } = this.props
return connectDragSource(
connectDropTarget(),
/* … */
)
}
}
export default flow(
DragSource(/* … */),
DropTarget(/* … */),
)(YourComponent)
Putting It All Together
以下にすでに作成したCard
コンポーネントをドラッグ元にラッピングする例を示します。
import React from 'react'
import { DragSource } from 'react-dnd'
// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
CARD: 'card',
}
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
const cardSource = {
beginDrag(props) {
// Return the data describing the dragged item
const item = { id: props.id }
return item
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return
}
// When dropped on a compatible target, do something
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
CardActions.moveCardToList(item.id, dropResult.listId)
},
}
/**
* Specifies which props to inject into your component.
*/
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging(),
}
}
function Card(props) {
// Your component receives its own props as usual
const { id } = props
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = props
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>,
)
}
// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)
これであなたは残りのReact DnDのドキュメントを読むのに十分な知識を身につけました!
tutorial を始めましょう。
Hooks API
useDrag
import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } from 'react-dnd'
const { useDrag } = dnd
function DraggableComponent(props) {
const [collectedProps, ref] = useDrag({
item: { id, type },
})
return <div ref={ref}>…</div>
}
パラメータ
- spec: 仕様オブジェクト。どう作るかの詳細は下を御覧ください。
返り値の配列
-
Index 0: コレクト関数から返ってくるコレクトプロパティを含んだオブジェクト。コレクト関数が定義されていなければ空のオブジェクトが返る。
-
Index 1: useするためのReactのref。specオブジェクトでrefが定義されていなければ、自動的に生成される。refはドラッグできる要素に付ける必要がある。
Spec オブジェクトの中身
-
item: 必須。 ドラッグされるデータを示すプレーンなjsのオブジェクト。
これはドロップ先のコンポーネントが唯一ドラッグする元について知れる情報。なので、ドロップ先が知るべき最小の情報を載せる。
おそらく複雑な参照をここで渡してしまうと思う。ただ、ドラッグ元とドロップ先は関連づいているので、これを避けるのは難しい。
なので、{ type, id }
みたいな形で返すのが望ましい.
item.type
は必須。string
かES6のsymbol
である必要がある。
ドロップターゲットに登録されているタイプと同じitem
だけが反応します。
items
とtypes
についてはoverviewを読んでください。 -
ref: オプショナル. refオブジェクトはドラッグできる要素を使うために付与します。セットされていなければ、1が作成され、返ります。
-
preview: オプショナル. ドラッグ中のプレビューとして用いるHTML要素かアタッチされているref要素. これを作成するのに
useDragPreview
を使うのを検討してみてください。 -
previewOptions: オプショナル. ドラッグのプレビューのオプションを示すためのプレーンなjsのオブジェクト
-
options: オプショナル.プレーンなオブジェクト。
コンポーネントのpropsがスカラー(単なる値や関数でないもの)でないなら、options
オブジェクトの中で独自のarePropsEqual(props, otherProps)
を指定することでパフォーマンスを改善できます。特にパフォーマンスの問題がなければ気にしなくて良いです。 -
begin(monitor): オプショナル. ドラッグが開始したときに発火する。
特に何も返す必要はないが、spec
のデフォルトのitem
プロパティをオーバーライドしているときはオブジェクトが返ります。 -
end(monitor): オプショナル. ドラッグが終わったとき、止まったときに呼ばれる。
begin
関数の呼び出し時に毎回end
関数が呼ばれることが保証されている。
互換性のあるドロップ先のコンポーネントかどうかをチェックするためにmonitor.didDrop()
を使うこともできます。これが呼び出されると、ドロップ先のdrop()
関数からプレーンなオブジェクトが返り、drop result
で指定されたドロップ先はmonitor.getDropResult()
として利用可能になる。
このメソッドはFluxのactionを呼ぶのに最適な場所です。
備考: もしコンポーネントがドラッグ中にunmountされた場合、コンポーネントのパラメータはnullがセットされます。
- canDrag(monitor): オプショナル. ドラッグが許可されているかどうかを指定するために使います。常に許可したい場合は除外するだけで構いません。
props
を通した記述からドラッグを禁止したいときに便利です。
備考: このメソッドの中では
monitor.canDrag()
は呼び出せません。
- isDragging(monitor): オプショナル. デフォルトではドラッグ操作を開始したドラッグ元がドラッグしているかを示します。カスタムした
isDragging
メソッドを定義することでこの挙動をオーバーライドすることができます。するとprops.id === monitor.getItem().id.
のような方たちで返ります。
オリジナルのコンポーネントがドラッグ中にunmountされてあとで違う親コンポーネントで復活させたい場合これを使いましょう
例えば、カンバンボードのようにリストをまたいでカードを動かすときに、カードにドラッグされている状態を保持させたい。技術的にはそのコンポーネントはunmountされ、違うリストに移動させるたびに毎回違うものがmountされる。
備考: このメソッドの中では
monitor.isDragging()
は呼び出せません。
- collect: オプショナル. コレクト関数。コンポーネントに注入するpropsのプレーンなオブジェクトを返します
monitor
とprops
という2つのパラメータを受け取ります。 monitorとcollecting関数に関してはoverviewを読んでください。コレクト関数についての詳細は次の章で。
useDragLayer
コンポーネントをdrag-layerとして使うためのhooks
import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } from 'react-dnd'
const { useDragLayer } = dnd
function DragLayerComponent(props) {
const collectedProps = useDragLayer(spec)
return <div>…</div>
}
パラメータ
-
collect: 必須. コレクト関数。コレクト関数。コンポーネントに注入するpropsのプレーンなオブジェクトを返します
monitor
とprops
という2つのパラメータを受け取ります。 monitorとcollecting関数に関してはoverviewを読んでください。コレクト関数についての詳細は次の章で。
返り値
コレクト関数からコレクトされたプロパティのオブジェクト
useDragPreview
drag-layerとしてコンポーネントを使用するためのhooks
import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } from 'react-dnd'
const { useDragPreview } = dnd
function DragLayerPreview(props) {
const [DragPreview, preview] = useDragPreview(spec)
const [collectedProps, ref] = useDrag({
item: { id, type },
preview,
})
return (
<>
<DragPreview />
<div ref={ref}>…drag item…</div>
</>
)
}
パラメータ
- dragPreview: ドラッグのプレビューをレンダリングするrefForwardingコンポーネント
返り値の配列
-
Index 0: renderメソッドでレンダリングするドラッグのプレビューのコンポーネント
-
Index 1: ドラッグプレビューのrefオブジェクト。useDragでの仕様を通して得られる。
useDrop
コンポーネントをドロップターゲットとして使うためのhooksです。
import { __EXPERIMENTAL_DND_HOOKS_THAT_MAY_CHANGE_AND_BREAK_MY_BUILD__ as dnd } from 'react-dnd'
const { useDrop } = dnd
function myDropTarget(props) {
const [collectedProps, ref] = useDrop({ accept })
return <div ref={ref}>Drop Target</div>
}
パラメータ
- spec: 仕様オブジェクト。どう作るかの詳細は下を御覧ください。
返り値の配列
-
Index 0:コレクト関数から返ってくるコレクトプロパティを含んだオブジェクト。コレクト関数が定義されていなければ空のオブジェクトが返る。
-
Index 1: useするためのReactのref。specオブジェクトでrefが定義されていなければ、自動的に生成される。refはドロップできるDOMのエリアに付ける必要がある。
Specification Object Members
-
accept: 必須. コンポーネントの
props
から受け取ったstring、ES6のsymbolもしくはそれらの配列もしくはそれらを返す関数が入る。
このドロップターゲットはドラッグ元から提供されているitemのtypeまたはtypeにあっているものだけが反応する。
itemsとtypesについてはoverviewを読んでください。 -
ref: オプショナル. ドラッグできる要素を使うために付与します。セットされていなければ、1が作成され、返ります。
-
options: オプショナル. プレーンなオブジェクト。 コンポーネントのpropsがスカラー(単なる値や関数でないもの)でないなら、
options
オブジェクトの中で独自のarePropsEqual(props, otherProps)
を指定することでパフォーマンスを改善できます。特にパフォーマンスの問題がなければ気にしなくて良いです。 -
drop(item, monitor): オプショナル. 互換性のあるitemをターゲットにドロップしたときにundefinedであってもプレーンなオブジェクトであっても呼ばれる。オブジェクトを返した際にはそれはdrop resultになり、drag元の
endDrag
メソッドでmonitor.getDropResult()
を通して利用可能になります。これはドロップを受け取るターゲットによって違うアクションをしたいときに便利です。 ネストされたドロップターゲットがある場合、monitor.didDrop()
とmonitor.getDropResult()
をチェックすることでネストされたターゲットがすでにdrop
をハンドリングしているかを確認することができます。この両方のメソッドとドラッグ元のsendDrag
メソッドはFluxのactionを発火するのに適した場所です。 このメソッドはcanDrop()
が定義されいて、false
を返す場合は呼び出されません。 -
hover(item, monitor): オプショナル. コンポーネントの上をitemがhoverしたときに呼ばれる。
monitor.isOver({ shallow: true })
をチェックすることでhoverが今のターゲットの上またはネストされたターゲットの上にあるかを確かめることができます。drop()
とは違い、このメソッドはcanDrop()
が定義されていて、false
を返すとしても呼ばれます。monitor.canDrop()
をチェックすることでそのケースかどうかを確かめることができます。 -
canDrop(item, monitor): オプショナル. ドロップターゲットがitemを許可しているかを特定するために使います。常に許可したい場合は除外するだけで構いません。
props
またはmonitor.getItem()
を通した記述からドロップを禁止したいときに便利です。
備考: このメソッドの中では
monitor.canDrop()
は呼び出せません。
- collect: 必須. コレクト関数。コレクト関数。コンポーネントに注入するpropsのプレーンなオブジェクトを返します
monitor
とprops
という2つのパラメータを受け取ります。 monitorとcollecting関数に関してはoverviewを読んでください。コレクト関数についての詳細は次の章で。
最後に
このドキュメントは執筆途中なので、リクエストや場違い指摘などあればコメント等お願いいたします!