Reactアプリケーションにドラッグ&ドロップを実装するのがReact DnDです。公式サイトの「Examples」には、作例がいくつかに分類されて掲載されています。
その中でも基本的な「Drag Around」にある「Naive」のサンプルのつくり方と構文を、記事「Create React App + React DnD: 単純なドラッグ&ドロップ」で解説しました。でき上がった作例「React DnD: Drag around naive」にTypeScriptの型づけを加えてみようというのが本稿のお題です。
React DnDの構文や作例のコードの組み立てについては、リンクした前出の記事をお読みください。
React + TypeScriptのひな形に作例のコードをコピーする
Reactアプリケーションのひな形は、Create React Appでつくります。React + TypeScriptのひな形とするオプションは--template typescriptです。プロジェクト名はreact-dnd-drag-around-naiveとしました。
npx create-react-app react-dnd-drag-around-naive --template typescript
また、React DnDとバックエンドのreact-dnd-html5-backendもインストールしてください(「Create React App + React DnD 02: ドラッグ&ドロップで動かす」01「コンポーネントをドラッグする」参照)。
npm install react-dnd react-dnd-html5-backend
そうしたら、作例「React DnD: Drag around naive」からコードをひな形にコピーします。ひな形に新たに加えなければならないのは、つぎの3つのモジュールです。拡張子はJavaScriptのjsxでなく、TypeScriptのtsxに変えます。
src/Box.tsxsrc/Container.tsxsrc/Example.tsx
モジュールsrc/App.tsxは、すでにひな形にありますので、つくりません。単に、作例からコードを上書きペーストするだけです。ルートモジュールは、これでできあがりました(図001)。コンポーネントAppに引数はありませんし、戻り値の型はJSX(JSX.Element)だと推論されるからです。
図001■ルートモジュールsrc/App.tsx
3つのモジュールについても、作例からまずはコードをそのままコピー&ペーストしてください。
モジュールsrc/Example.tsxとsrc/Box.tsx
3つのモジュールのうち、src/Example.tsxとsrc/Box.tsxを先に採り上げます。コピーしたコードから、TypeScriptにしたがって書き替える部分を絞って見ていきましょう。
src/Example.tsxは、関数コンポーネントExampleの型をReact.VFCで定めるだけです。もっとも、前掲ルートコンポーネントAppと同じく、引数がなく戻り値のJSXは推論できます。したがって、型指定しなくても構いません。本稿のお題がTypeScriptによる型づけなので、ここは加えておくことにしましょう。
export const Example: React.VFC = () => {
};
モジュールsrc/Box.tsxには、JSX要素のstyleプロパティに与えるオブジェクトの定め(style)があります。その型づけはReact.CSSPropertiesです。また、Boxコンポーネントは、プロパティを引数に受け取ります。したがって、インタフェースBoxPropsを定義して、関数コンポーネントの型React.VFCにジェネリックで与えました。
const style: React.CSSProperties = {
};
export interface BoxProps {
id: string;
left: number;
top: number;
hideSourceOnDrag?: boolean;
children: React.ReactNode;
}
export const Box: React.VFC<BoxProps> = ({
}) => {
}
モジュールsrc/Container.tsx
今回、手を加える部分が多いのは、モジュールsrc/Container.tsxです。
ドラッグする要素の仕様オブジェクトの型(DragItem)は、新たなモジュールsrc/interfaces.tsにインタフェースとして定めました。あとあと、他のモジュールからも参照して用いられるかもしれないからです。
export interface DragItem {
type: string;
id: string;
top: number;
left: number;
}
モジュールsrc/Container.tsxは、前掲インタフェースDragItemに加え、react-dndから型XYCoordをimportします。用いられるのはいずれも、useDropの引数コールバックが返す仕様オブジェクトのdrop()メソッドです。メソッドの第1引数(item)がDragItemで型づけられ、getDifferenceFromInitialOffset()の戻り値をXYCoordで型アサーションします。
さらに、インタフェースBoxStateを定め、useStateフックにジェネリックで型指定しました。これで、前掲作例がTypeScriptのプロジェクトに書き替えられました(図002)。サンプルはCodeSandboxに公開しますので、各モジュールのコードや動きはこちらでお確かめください。
// import { useDrop } from "react-dnd";
import { useDrop, XYCoord } from "react-dnd";
import { DragItem } from "./interfaces";
export interface ContainerProps {
hideSourceOnDrag: boolean;
}
export interface BoxState {
[key: string]: { top: number; left: number; title: string };
}
const styles: React.CSSProperties = {
};
export const Container: React.VFC<ContainerProps> = ({ hideSourceOnDrag }) => {
const [boxes, setBoxes] = useState<BoxState>({
});
const [, drop] = useDrop(
() => ({
drop(item: DragItem, monitor) {
const { x, y } = monitor.getDifferenceFromInitialOffset() as XYCoord;
);
}
図002■React DnD + TypeScript: 単純なドラッグ&ドロップ
公式サイトのTypeScript作例との違い
React DnD公式サイトのDrag Around「Naive」にもTypeScriptの作例が公開されています。モジュールsrc/Container.tsxについて、本稿のコードと異なる部分をふたつだけ補いましょう。
ひつは、インタフェースです。公式サイトの作例には、つぎのようなContainerStateが定められています。けれど、どこにも用いられていません。おそらく、useStateフックのジェネリックに使おうとしたのではないかと推測します。けれど、実際に書き加えられているのは型注釈です。
export interface ContainerState {
boxes: { [key: string]: { top: number; left: number; title: string } }
}
export const Container: FC<ContainerProps> = ({ hideSourceOnDrag }) => {
const [boxes, setBoxes] = useState<{
[key: string]: {
top: number
left: number
title: string
}
}>({
})
}
本項では前掲コードのとおり、インタフェースBoxStateを定めてuseStateのジェネリックに加えました。
もうひとつは、XYCoord型のデータから必要なプロパティを分割代入で取り出したことです。細かい点ではあるものの、コードがスッキリします。
// const delta = monitor.getDifferenceFromInitialOffset() as XYCoord // 公式作例
const { x, y } = monitor.getDifferenceFromInitialOffset() as XYCoord;

