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.tsx
src/Container.tsx
src/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;