mcpField
R
reanimated-dnd

reanimated-dnd

github.com/entropyconquers開発1.1k196 回インストール32 週間前に更新
リポジトリ

reanimated-dnd について

React Native 開発者向けのドラッグ&ドロップ UI 統合スキルです。コンポーネントと低レベルフックを備え、並べ替え可能なリストやグリッドに対応し、アニメーションは UI スレッドで実行します。

主な機能

  1. 01ドラッグ&ドロップ用コンポーネントと領域
  2. 02縦横方向の並べ替え可能なリスト
  3. 03挿入・交換方式のグリッド並べ替え
  4. 04軸・範囲・衝突の制約
  5. 05高さを測定できるリスト
  6. 06Reanimated worklets によるアニメーション

活用シーン

  1. 01UI の領域間で項目を移動
  2. 02並べ替え可能なリストやグリッドを構築
  3. 03ドラッグ方向と範囲を制限
  4. 04高さが異なるリスト項目に対応

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/entropyconquers/react-native-reanimated-dnd --skill reanimated-dnd

インストール後、Claude Code で/reanimated-dndを入力して読み込まれたか確認します。

詳細

react-native-reanimated-dnd 統合スキル

バージョン: 2.0.0 カテゴリ: UI/ドラッグ&ドロップ プラットフォーム: React Native(react-native-reanimated >=4.2.0、react-native-gesture-handler >=2.28.0、react-native-worklets >=0.7.0 が必要)


概要

react-native-reanimated-dnd は、React Native 向けの高性能なドラッグ&ドロップ用プリミティブを提供します。高レベルコンポーネントと低レベルフックの両方を備えており、次の用途に使用できます。

  • ドラッグ&ドロップ:ドロップゾーン間でアイテムを移動します
  • 並べ替え可能なリスト:縦方向および横方向に並べ替え可能なリスト
  • 並べ替え可能なグリッド:挿入または入れ替えによる並べ替えが可能な2次元グリッド
  • 制約:軸の固定、ドラッグ範囲の制限、衝突検出
  • 動的な高さ:リスト内の可変高アイテムを自動計測します

すべてのアニメーションは、Reanimated のワークレットを介して UI スレッド上で実行されます。


インストール

npm install react-native-reanimated-dnd
# or
yarn add react-native-reanimated-dnd
ピア依存関係(個別にインストールする必要があります)
npm install react-native-reanimated react-native-gesture-handler react-native-worklets
必須のセットアップ

アプリのルートを GestureHandlerRootView でラップします。

import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function App() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      {/* Your app content */}
    </GestureHandlerRootView>
  );
}

コアアーキテクチャ

DropProvider (context - required for Draggable/Droppable)
├── Draggable (items that can be picked up)
│   └── Draggable.Handle (optional restricted drag area)
└── Droppable (zones that accept drops)

Sortable (self-contained vertical/horizontal list)
├── SortableItem (individual reorderable item)
│   └── SortableItem.Handle (optional restricted drag area)

SortableGrid (self-contained 2D grid)
├── SortableGridItem (individual grid cell)
│   └── SortableGridItem.Handle (optional restricted drag area)

重要なルール:追跡のため、すべてのデータアイテムに id: string プロパティを必ず指定してください。


パターン1:基本的なドラッグ&ドロップ

DropProvider + Draggable + Droppable を使用して、アイテムをドロップゾーンに移動します。

import {
  DropProvider,
  Draggable,
  Droppable,
} from 'react-native-reanimated-dnd';

function DragDropExample() {
  const [droppedItem, setDroppedItem] = useState<string | null>(null);

  return (
    <DropProvider>
      <View style={styles.items}>
        <Draggable data={{ id: '1', label: 'Item A' }}>
          <View style={styles.item}>
            <Text>Item A</Text>
          </View>
        </Draggable>

        <Draggable data={{ id: '2', label: 'Item B' }}>
          <View style={styles.item}>
            <Text>Item B</Text>
          </View>
        </Draggable>
      </View>

      <Droppable onDrop={(data) => setDroppedItem(data.label)}>
        <View style={styles.dropZone}>
          <Text>{droppedItem ?? 'Drop here'}</Text>
        </View>
      </Droppable>
    </DropProvider>
  );
}
Draggable の Props
Prop型デフォルト説明
dataTData必須ドロップハンドラーに渡されるペイロード
draggableIdstring自動一意の識別子
dragDisabledbooleanfalseドラッグを無効にします
preDragDelaynumber0ドラッグ開始前の遅延時間(ミリ秒)
dragAxis"x" | "y" | "both""both"移動軸を制限します
dragBoundsRefRefObject<View>-ビュー内に移動を制限します
collisionAlgorithm"center" | "intersect" | "contain""intersect"ドロップ可能な領域との重なりを検出する方法
animationFunction(toValue: number) => number-カスタムの復帰アニメーション
onDragStart(data: TData) => void-ドラッグ開始時に呼び出されます
onDragEnd(data: TData) => void-ドラッグ終了時に呼び出されます
onDragging({ x, y, tx, ty, itemData }) => void-リアルタイムの位置更新
onStateChange(state: DraggableState) => void-状態遷移時のコールバック
Droppable の Props
Prop型デフォルト説明
onDrop(data: TData) => void必須ドロップされたアイテムを処理します
dropDisabledbooleanfalseドロップを無効にします
capacitynumber1許可されるアイテムの最大数
dropAlignmentDropAlignment"center"ドロップされたアイテムの位置合わせ
dropOffset{ x: number, y: number }-位置合わせ後の位置を微調整します
activeStyleStyleProp<ViewStyle>-アイテムが上にあるときに適用されるスタイル
onActiveChange(isActive: boolean) => void-ホバー状態の変化時に呼び出されます
droppableIdstring自動ドロップゾーンの一意の識別子
DropProvider の Props
Prop型説明
onDroppedItemsUpdate(items: DroppedItemsMap) => voidすべてのゾーンにわたるアイテムを追跡します
onDragging({ x, y, tx, ty, itemData }) => voidドラッグ位置をグローバルに追跡します
onDragStart(data) => voidドラッグ開始時に呼び出されます
onDragEnd(data) => voidドラッグ終了時に呼び出されます
onLayoutUpdateComplete() => voidレイアウトの更新完了時に呼び出されます
DropAlignment の値

"center" | "top-left" | "top-center" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-center" | "bottom-right"


パターン2:ドラッグハンドル

Draggable.Handle を使用して、ドラッグ可能な領域を特定のハンドル領域に限定します。

<Draggable data={{ id: '1', label: 'Card' }}>
  <View style={styles.card}>
    <Draggable.Handle>
      <View style={styles.handleBar}>
        <Text>Drag here</Text>
      </View>
    </Draggable.Handle>
    <View style={styles.cardContent}>
      <Text>This area does NOT initiate drag</Text>
    </View>
  </View>
</Draggable>

同じパターンを、並べ替え可能なアイテムの SortableItem.Handle にも使用できます。


パターン3:縦方向の並べ替え可能なリスト

並べ替え可能なリストには Sortable コンポーネントを使用します。

import { Sortable, SortableItem } from 'react-native-reanimated-dnd';

interface Item {
  id: string;
  title: string;
}

const ITEM_HEIGHT = 60;

function SortableListExample() {
  const [items, setItems] = useState<Item[]>([
    { id: '1', title: 'First' },
    { id: '2', title: 'Second' },
    { id: '3', title: 'Third' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.listItem}>
        <Text>{item.title}</Text>
      </View>
    </SortableItem>
  ), []);

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      itemHeight={ITEM_HEIGHT}
    />
  );
}
Sortable Props
PropTypeDefaultDescription
dataTData[]requiredアイテム(それぞれに id: string が必要です)
renderItem(props) => ReactNoderequiredアイテムのレンダラー
direction"vertical" | "horizontal""vertical"リストの方向
itemHeightnumber | number[] | (item, i) => number-アイテムの高さ(垂直方向では必須)
itemWidthnumber-アイテムの幅(水平方向では必須)
gapnumber0アイテム間の間隔(水平方向のみ)
paddingHorizontalnumber0コンテナーのパディング(水平方向のみ)
enableDynamicHeightsbooleanfalseアイテムの高さを自動測定します
estimatedItemHeightnumber60未測定アイテムのフォールバックの高さ
onHeightsMeasured(heights) => void-測定した高さを引数として呼び出されます
useFlatListbooleantrue仮想化に FlatList を使用します
itemKeyExtractor(item, index) => stringitem.idカスタムキー抽出関数
styleStyleProp<ViewStyle>-ScrollView/FlatList のスタイル
contentContainerStyleStyleProp<ViewStyle>-コンテンツコンテナーのスタイル
SortableItem Props
PropTypeDescription
idstringアイテムの一意な識別子
dataTアイテムデータ
onMove(id, from, to) => void位置が変更されたときに呼び出されます
onDragStart(id, position) => voidドラッグ開始時
onDrop(id, position, allPositions?) => voidアイテムがドロップされたとき
onDragging(id, overItemId, yPosition) => voidドラッグ位置をリアルタイムで通知します
styleStyleProp<ViewStyle>コンテナーのスタイル
animatedStyleStyleProp<ViewStyle>アニメーションスタイル

Pattern 4: 水平方向の Sortable リスト

import {
  Sortable,
  SortableItem,
  SortableDirection,
} from 'react-native-reanimated-dnd';

const ITEM_WIDTH = 120;

function HorizontalSortableExample() {
  const [items, setItems] = useState([
    { id: '1', label: 'Tag A' },
    { id: '2', label: 'Tag B' },
    { id: '3', label: 'Tag C' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.tag}>
        <Text>{item.label}</Text>
      </View>
    </SortableItem>
  ), []);

  return (
    <Sortable
      data={items}
      renderItem={renderItem}
      direction={SortableDirection.Horizontal}
      itemWidth={ITEM_WIDTH}
      gap={12}
      paddingHorizontal={12}
    />
  );
}

Pattern 5: Sortable グリッド

import {
  SortableGrid,
  SortableGridItem,
  GridOrientation,
  GridStrategy,
} from 'react-native-reanimated-dnd';

function GridExample() {
  const [items, setItems] = useState([
    { id: '1', label: 'A' },
    { id: '2', label: 'B' },
    { id: '3', label: 'C' },
    { id: '4', label: 'D' },
    { id: '5', label: 'E' },
    { id: '6', label: 'F' },
  ]);

  const renderItem = useCallback(({ item, ...props }) => (
    <SortableGridItem
      key={item.id}
      id={item.id}
      data={item}
      onMove={(id, from, to) => {
        setItems(prev => {
          const next = [...prev];
          const [moved] = next.splice(from, 1);
          next.splice(to, 0, moved);
          return next;
        });
      }}
      {...props}
    >
      <View style={styles.gridCell}>
        <Text>{item.label}</Text>
      </View>
    </SortableGridItem>
  ), []);

  return (
    <SortableGrid
      data={items}
      renderItem={renderItem}
      dimensions={{
        columns: 3,
        itemWidth: 100,
        itemHeight: 100,
        rowGap: 8,
        columnGap: 8,
      }}
      orientation={GridOrientation.Vertical}
      strategy={GridStrategy.Insert}
    />
  );
}
SortableGrid Props
PropTypeDefaultDescription
dataTData[]requiredグリッドアイテム(それぞれに id: string が必要です)
renderItem(props) => ReactNoderequiredアイテムのレンダラー
dimensionsGridDimensionsrequiredグリッド設定(以下を参照)
orientation"vertical" | "horizontal""vertical"グリッドの向き
strategy"insert" | "swap""insert"アイテムの並べ替え方法:insert は他のアイテムをずらし、swap は 2 つのアイテムを入れ替えます
scrollEnabledbooleantrueスクロールを有効にします
itemKeyExtractor(item, index) => stringitem.idカスタムキー抽出
styleStyleProp<ViewStyle>-ScrollView のスタイル
contentContainerStyleStyleProp<ViewStyle>-コンテンツのスタイル
GridDimensions
PropTypeDescription
columnsnumber列数(垂直方向の場合)
rowsnumber行数(水平方向の場合)
itemWidthnumberアイテムの幅
itemHeightnumberアイテムの高さ
rowGapnumber行間の間隔
columnGapnumber列間の間隔
SortableGridItem Props
PropTypeDescription
idstring一意な識別子
dataTアイテムデータ
activationDelaynumberドラッグ開始までの遅延時間(ミリ秒)
onMove(id, from, to) => void位置が変更されました
onDragStart(id, position) => voidドラッグ開始時
onDrop(id, position, allPositions?) => voidアイテムがドロップされたとき
onDragging(id, overItemId, x, y) => void位置をリアルタイムで通知します
isBeingRemovedboolean削除アニメーションを開始します
styleStyleProp<ViewStyle>コンテナーのスタイル
animatedStyleStyleProp<ViewStyle>アニメーションスタイル

Pattern 6: 動的な高さ

アイテムの高さが可変のリストの場合:

<Sortable
  data={items}
  renderItem={renderItem}
  enableDynamicHeights
  estimatedItemHeight={80}
  onHeightsMeasured={(heights) => {
    // { [id]: measuredHeight }
  }}
/>

または、アイテムごとに高さを明示的に指定します:

<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={(item, index) => item.expanded ? 120 : 60}
/>

// Or as an array:
<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={[60, 80, 120, 60, 100]}
/>

Pattern 7: 軸の制約

ドラッグを単一の軸に制限します:

// Horizontal only
<Draggable data={data} dragAxis="x">
  <View>{/* content */}</View>
</Draggable>

// Vertical only
<Draggable data={data} dragAxis="y">
  <View>{/* content */}</View>
</Draggable>

パターン 8:ドラッグ範囲の制限

コンテナ内でドラッグできる範囲を制限します:

function BoundedExample() {
  const boundsRef = useRef<View>(null);

  return (
    <DropProvider>
      <View ref={boundsRef} style={styles.boundary}>
        <Draggable data={{ id: '1' }} dragBoundsRef={boundsRef}>
          <View style={styles.item}>
            <Text>Cannot escape boundary</Text>
          </View>
        </Draggable>
      </View>
    </DropProvider>
  );
}

軸の制約と組み合わせます:

<Draggable data={data} dragBoundsRef={boundsRef} dragAxis="y">
  {/* Vertical movement only, within bounds */}
</Draggable>

パターン 9:衝突検出

3 つのアルゴリズムによって、ドラッグ可能なアイテムがドロップ可能な領域をいつ「アクティブ化」するかを制御します:

// Default: any overlap triggers
<Draggable data={data} collisionAlgorithm="intersect">

// Center point must be over the droppable
<Draggable data={data} collisionAlgorithm="center">

// Entire draggable must be inside the droppable
<Draggable data={data} collisionAlgorithm="contain">

パターン 10:ドロップゾーンの容量

ゾーンが受け入れるアイテム数を制限します:

<Droppable onDrop={handleDrop} capacity={1}>
  {/* Accepts exactly one item */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={3}>
  {/* Accepts up to three items */}
</Droppable>

<Droppable onDrop={handleDrop} capacity={Infinity}>
  {/* Unlimited */}
</Droppable>

パターン 11:ゾーン間でのアイテムの追跡

DropProvider.onDroppedItemsUpdate を使って、各ゾーンにどのアイテムがあるかを追跡します:

<DropProvider
  onDroppedItemsUpdate={(droppedItems) => {
    // droppedItems is: { [draggableId]: { droppableId, data } }
    setMapping(droppedItems);
  }}
>
  {/* Draggables and Droppables */}
</DropProvider>

ref を介してプログラムからアクセスします:

const providerRef = useRef<DropProviderRef>(null);

<DropProvider ref={providerRef}>
  {/* ... */}
</DropProvider>

// Later:
const items = providerRef.current?.getDroppedItems();
providerRef.current?.requestPositionUpdate();

パターン 12:ドロップゾーンのアクティブ時のスタイル

アイテムがドロップゾーンに重なったときに視覚的なフィードバックを表示します:

<Droppable
  onDrop={handleDrop}
  activeStyle={{
    borderColor: 'blue',
    borderWidth: 2,
    backgroundColor: 'rgba(0, 0, 255, 0.1)',
  }}
  onActiveChange={(isActive) => {
    // true when item is hovering, false when it leaves
  }}
>
  <View style={styles.zone}>
    <Text>Drop Zone</Text>
  </View>
</Droppable>

パターン 13:カスタムアニメーション

アイテムが元の位置または所定の位置に戻るときのカスタムアニメーションを指定します:

import { withSpring, withTiming, Easing } from 'react-native-reanimated';

// Spring animation
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withSpring(toValue, { damping: 15, stiffness: 150 })
  }
>

// Timing animation with easing
<Draggable
  data={data}
  animationFunction={(toValue) =>
    withTiming(toValue, { duration: 300, easing: Easing.bezier(0.25, 0.1, 0.25, 1) })
  }
>

パターン 14:ドラッグ状態の追跡

ドラッグのライフサイクルを監視します:

import { DraggableState } from 'react-native-reanimated-dnd';

<Draggable
  data={data}
  onStateChange={(state) => {
    // DraggableState.IDLE | DraggableState.DRAGGING | DraggableState.DROPPED
  }}
  onDragStart={(data) => { /* drag began */ }}
  onDragEnd={(data) => { /* drag ended */ }}
  onDragging={({ x, y, tx, ty, itemData }) => {
    // real-time position while dragging
  }}
>

フック API(低レベル)

完全に制御するには、コンポーネントの代わりにフックを使います:

useDraggable
import { useDraggable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

function CustomDraggable({ data }) {
  const {
    animatedViewProps,  // { style, onLayout }
    gesture,            // pan gesture
    state,              // DraggableState
  } = useDraggable({
    data,
    onDragStart: (d) => {},
    onDragEnd: (d) => {},
    collisionAlgorithm: 'intersect',
  });

  return (
    <GestureDetector gesture={gesture}>
      <Animated.View {...animatedViewProps}>
        <Text>Custom Draggable</Text>
      </Animated.View>
    </GestureDetector>
  );
}
useDroppable
import { useDroppable } from 'react-native-reanimated-dnd';

function CustomDropZone() {
  const {
    viewProps,   // { onLayout, style? }
    isActive,    // boolean
  } = useDroppable({
    onDrop: (data) => {},
    capacity: 1,
    activeStyle: { borderColor: 'green' },
  });

  return (
    <Animated.View {...viewProps}>
      <Text>{isActive ? 'Release to drop' : 'Drop here'}</Text>
    </Animated.View>
  );
}
useSortableList + useSortable

カスタムのソート可能なリストを実装する場合:

import { useSortableList, useSortable } from 'react-native-reanimated-dnd';
import { GestureDetector } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';

function CustomSortableList({ data }) {
  const {
    positions,
    scrollY,
    autoScroll,
    scrollViewRef,
    handleScroll,
    handleScrollEnd,
    contentHeight,
    getItemProps,
  } = useSortableList({ data, itemHeight: 60 });

  return (
    <Animated.ScrollView
      ref={scrollViewRef}
      onScroll={handleScroll}
      onMomentumScrollEnd={handleScrollEnd}
      style={{ height: 400 }}
      contentContainerStyle={{ height: contentHeight }}
    >
      {data.map((item, index) => (
        <CustomSortableItem
          key={item.id}
          item={item}
          {...getItemProps(item, index)}
        />
      ))}
    </Animated.ScrollView>
  );
}

function CustomSortableItem({ item, ...sortableProps }) {
  const {
    animatedStyle,
    panGestureHandler,
  } = useSortable({
    ...sortableProps,
    onMove: (id, from, to) => {},
  });

  return (
    <GestureDetector gesture={panGestureHandler}>
      <Animated.View style={animatedStyle}>
        <Text>{item.title}</Text>
      </Animated.View>
    </GestureDetector>
  );
}
useHorizontalSortableList + useHorizontalSortable

垂直方向の場合と同じパターンですが、水平方向のリストに使用します:

import {
  useHorizontalSortableList,
  useHorizontalSortable,
} from 'react-native-reanimated-dnd';

const {
  positions,
  scrollX,
  autoScroll,
  scrollViewRef,
  handleScroll,
  handleScrollEnd,
  contentWidth,
  getItemProps,
} = useHorizontalSortableList({
  data,
  itemWidth: 120,
  gap: 12,
  paddingHorizontal: 12,
});
useGridSortableList + useGridSortable

カスタムのグリッドを実装する場合:

import {
  useGridSortableList,
  useGridSortable,
  GridOrientation,
  GridStrategy,
} from 'react-native-reanimated-dnd';

const {
  positions,
  scrollY,
  scrollX,
  autoScrollDirection,
  scrollViewRef,
  handleScroll,
  handleScrollEnd,
  contentWidth,
  contentHeight,
  getItemProps,
} = useGridSortableList({
  data,
  dimensions: { columns: 3, itemWidth: 100, itemHeight: 100 },
  orientation: GridOrientation.Vertical,
  strategy: GridStrategy.Insert,
});

型リファレンス

// All sortable data must have an id
interface SortableData {
  id: string;
}

// Drag state enum
enum DraggableState {
  IDLE = "IDLE",
  DRAGGING = "DRAGGING",
  DROPPED = "DROPPED",
}

// Scroll directions
enum ScrollDirection { None, Up, Down }
enum HorizontalScrollDirection { None, Left, Right }

// List direction
enum SortableDirection { Vertical = "vertical", Horizontal = "horizontal" }

// Grid types
enum GridOrientation { Vertical = "vertical", Horizontal = "horizontal" }
enum GridStrategy { Insert = "insert", Swap = "swap" }
enum GridScrollDirection { None, Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight }

interface GridDimensions {
  columns?: number;
  rows?: number;
  itemWidth: number;
  itemHeight: number;
  rowGap?: number;
  columnGap?: number;
}

interface GridPosition {
  index: number;
  row: number;
  column: number;
  x: number;
  y: number;
}

// Collision detection
type CollisionAlgorithm = "center" | "intersect" | "contain";

// Drop alignment
type DropAlignment =
  | "center" | "top-left" | "top-center" | "top-right"
  | "center-left" | "center-right"
  | "bottom-left" | "bottom-center" | "bottom-right";

// Dropped items tracking
interface DroppedItemsMap<TData = unknown> {
  [draggableId: string]: {
    droppableId: string;
    data: TData;
  };
}

// DropProvider imperative handle
interface DropProviderRef {
  requestPositionUpdate: () => void;
  getDroppedItems: () => DroppedItemsMap;
}

// Grid positions shared value type
interface GridPositions {
  [id: string]: GridPosition;
}

よく使われるレシピ

ハンドル付きのソート可能なリスト
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <SortableItem.Handle>
      <View style={styles.handle}>
        <Text>|||</Text>
      </View>
    </SortableItem.Handle>
    <Text style={styles.content}>{item.title}</Text>
  </View>
</SortableItem>
入れ替えによる並べ替えに対応したグリッド
<SortableGrid
  data={items}
  renderItem={renderItem}
  dimensions={{ columns: 4, itemWidth: 80, itemHeight: 80 }}
  strategy={GridStrategy.Swap}
/>
ドラッグハンドル付きのグリッドアイテム
<SortableGridItem id={item.id} data={item} {...props}>
  <View style={styles.cell}>
    <SortableGridItem.Handle>
      <View style={styles.handle}>
        <Text>Drag</Text>
      </View>
    </SortableGridItem.Handle>
    <Text>{item.label}</Text>
  </View>
</SortableGridItem>
アクティブ化に遅延を設けたグリッド(誤操作によるドラッグを防止)
<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={300}
  {...props}
>
ドラッグ可能なアイテムのドラッグ開始前の遅延
<Draggable data={data} preDragDelay={200}>
  {/* Must hold 200ms before drag activates */}
</Draggable>
オフセットを使ったドロップ位置の調整
<Droppable
  onDrop={handleDrop}
  dropAlignment="top-left"
  dropOffset={{ x: 10, y: 10 }}
>
ソート可能なリストへのアイテムの動的な追加

アイテムには常に一意の id フィールドが必要です。状態配列を更新するだけです:

const addItem = () => {
  setItems(prev => [...prev, { id: String(Date.now()), title: 'New Item' }]);
};
アニメーション付きでグリッドからアイテムを削除する
<SortableGridItem isBeingRemoved={item.removing} {...props}>

注意点と避けるべきこと

データ要件
  1. すべてのアイテムに id: string が必須です — ID がない、または未定義の場合、並べ替えが正常に動作せず、エラーが通知されないまま失敗します。開発モードでは console.error が出力されますが、本番環境では通知なしに動作しなくなります。
  2. ID は一意である必要があります — ID が重複すると、複数のアイテムが同じ位置を共有します。このライブラリは内部で id -> index にマッピングしているため、重複した ID は互いに上書きされます。
  3. アイテムオブジェクトを変更しないでください — 状態を更新するときは、常に新しい配列やオブジェクトを作成してください。このライブラリはジェスチャーのクロージャ内にデータを保持するため、元のオブジェクトを変更すると、コールバック内のデータが古くなります。
コンテキストとラップ
  1. Draggable/Droppable には DropProvider の祖先が必須です — 祖先に DropProvider がないと、ドロップは通知なしに失敗します(クラッシュせず、本番環境ではエラーも発生しません)。Draggable/Droppable の使用箇所は必ず <DropProvider> でラップしてください。
  2. Sortable または SortableGrid を DropProvider でラップしないでください — これらのコンポーネントは独自の DropProvider を内部で作成します。プロバイダーをネストすると、衝突検出が正常に動作しなくなります。
  3. Sortable または SortableGrid を GestureHandlerRootView でラップしないでください — これらのコンポーネントは内部で自身をラップします。二重にラップすると、ジェスチャーが競合します。GestureHandlerRootView は、Draggable/Droppable パターンではアプリのルートでのみ使用してください。
  4. ハンドルコンポーネントは、親の直下の子である必要があります — Draggable.Handle は Draggable の内側、SortableItem.Handle は SortableItem の内側、SortableGridItem.Handle は SortableGridItem の内側に配置する必要があります。これらを別の場所で使用すると、子要素はレンダリングされますが、ドラッグ機能は動作しません(開発モードでは警告が表示されます)。
必須プロパティ(指定がないとエラーになります)
  1. 垂直方向の Sortable には itemHeight が必須です — 固定値、配列、関数のいずれかを渡すか、enableDynamicHeights={true} を設定してください。指定がない場合はエラーが発生します。
  2. 水平方向の Sortable には itemWidth が必須です — 動的な幅を指定するモードはありません。固定値を指定する必要があります。
  3. グリッドの寸法には、itemWidth、itemHeight、columns(垂直方向)または rows(水平方向)が必要です — いずれかが指定されていない場合はエラーが発生します。
状態管理
  1. onMove では状態配列を必ず更新してください — このライブラリは位置を視覚的にアニメーションするだけです。onMove で data 配列を並べ替えないと、画面上の順序とデータの順序が一致しなくなります。
// CORRECT
onMove={(id, from, to) => {
  setItems(prev => {
    const next = [...prev];
    const [moved] = next.splice(from, 1);
    next.splice(to, 0, moved);
    return next;
  });
}}

// WRONG — visual reorder happens but data stays stale
onMove={(id, from, to) => {
  console.log('moved', id, from, to); // not updating state!
}}
  1. onDragStart/onDragEnd 内で外部状態から data props を読み取らないでください — これらのコールバックは、ジェスチャーの作成時点の data をキャプチャします。ジェスチャーの作成後、コールバックが実行されるまでにデータオブジェクトが変わると、コールバックは古いバージョンを参照します。代わりに、コールバックに渡される data 引数を使用してください。

  2. Droppable のデフォルト容量は無限ではなく 1 です — capacity を設定しない場合、各ゾーンにドロップできるアイテムは 1 つだけです。無制限にするには capacity={Infinity} を設定してください。

アニメーション
  1. animationFunction は Reanimated のアニメーション値を必ず返す必要があります — worklet を介して UI スレッドで実行されます。必ず withSpring、withTiming、または別の Reanimated アニメーションを返してください。通常の数値を返すと、アニメーションは完全に省略されます(アイテムは遷移せず、瞬時に移動します)。
// CORRECT — returns a Reanimated animation
animationFunction={(toValue) => withSpring(toValue, { damping: 15 })}

// CORRECT — timing with easing
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}

// BAD — returns plain number, item snaps with no animation
animationFunction={(toValue) => toValue}
  1. デフォルトのアニメーションは withSpring(弾む動き)です — デフォルトでは、アイテムは元の位置に跳ね返ります。素早く動く感じにしたい場合は、withTiming を指定してください。
animationFunction={(toValue) => withTiming(toValue, { duration: 200 })}
  1. Droppable の activeStyle はアニメーションしません — 遷移せず、即座に適用されます。アニメーション付きのホバー時の反応が必要な場合は、Reanimated の useAnimatedStyle と onActiveChange を使用してください。
ジェスチャー処理
  1. ソート可能なアイテムの長押しによるアクティベーション時間は 200ms に固定されています — SortableItem や横方向のソート機能では、この時間をカスタマイズできません。activationDelay を指定できるのは SortableGridItem のみです。これにより誤操作によるドラッグを防げますが、ソート可能なアイテムを操作するには、常に短時間押し続ける必要があります。

  2. Draggable での preDragDelay={0} は ScrollView と競合します — 遅延が 0 の場合、パンジェスチャーが即座に有効になり、スクロールジェスチャーからタッチを奪います。Draggable が ScrollView 内にある場合は、preDragDelay={100} 以上を使用してください。

  3. Droppable が重なっている場合は、最初に登録されたものが優先されます — 2 つの Droppable が空間的に重なっている場合、最初に登録されたものが衝突判定で優先されます。「中心に最も近いもの」を選ぶアルゴリズムはありません。Droppable が重ならないようにしてください。

  4. ドラッグ可能なアイテムが Droppable より大きい場合、contain による衝突判定は発生しません — ドラッグ可能なアイテム全体が Droppable の内側に収まる必要があります。アイテムのほうが大きい場合は、代わりに "intersect" または "center" を使用してください。

プラットフォーム固有の事項
  1. ドラッグ時の影は iOS 専用です — アイテムのドラッグ中、ライブラリは視覚的なフィードバックとして shadowColor/shadowOpacity/shadowRadius を適用します。これらは iOS 専用のプロパティです。Android ではドラッグ時の影は表示されません。Android でフィードバックが必要な場合は、animatedStyle を介して独自に elevation を適用してください。

  2. Android では collapsable={false} が重要です — ライブラリは Draggable/Droppable のビューに内部でこれを設定します。フック API(useDraggable/useDroppable)をカスタムビューとともに使用する場合は、Animated.View に collapsable={false} を必ず設定してください。設定しないと、Android がネイティブビューを最適化で削除し、計測値が 0 になります。

// When using hooks directly
<Animated.View collapsable={false} {...animatedViewProps}>
パフォーマンス
  1. Sortable はデータ配列が変わるたびにリスト全体を再マウントします — Sortable はすべてのアイテム ID のハッシュを React のキーとして使用するため、データが変わるたび(並べ替え、追加、削除を含む)に完全な再マウントが発生します。これにより、スクロール位置とすべてのアニメーション状態がリセットされます。データが頻繁に変わる場合は、より細かく制御できるフック API(useSortableList + useSortable)の使用を検討してください。

  2. SortableGrid はデータが変更されても再マウントしません — Sortable とは異なり、SortableGrid は完全な再マウントを行わず、より効率的にデータの変更を処理します。

  3. onDragging ハンドラーは軽量に保ってください — 約 20 回/秒(50ms のスロットリング)で呼び出され、UI スレッドから JS に処理が渡されます。これらのコールバックで重い処理を行うと、動作がカクつきます。

  4. Sortable の backgroundColor: "white" は固定値です — 内部の ScrollView/FlatList には backgroundColor: "white" が固定で設定されています。ダークモードでは、style prop を使って上書きしてください。

<Sortable
  data={items}
  renderItem={renderItem}
  itemHeight={60}
  style={{ backgroundColor: '#1a1a1a' }}
/>
動的な高さ
  1. 1px 未満の動的な高さの変更は無視されます — このライブラリは高さを丸め、無限の再レンダリングループを防ぐために 1px 未満の変更を無視します。サブピクセル単位の高さ調整は破棄されます。

  2. 初期の動的な高さは、最初のデータスナップショットから一度だけ計算されます — 初回レンダリングの完了前にデータが変更されると、項目が再計測されるまで高さが古いままになることがあります。


ベストプラクティス

データ構造
// GOOD — simple, flat data with string IDs
const items = [
  { id: '1', title: 'Item 1' },
  { id: '2', title: 'Item 2' },
];

// GOOD — use stable unique IDs (not array indices)
const items = tasks.map(task => ({ ...task, id: task.uuid }));

// BAD — numeric IDs (must be strings)
const items = [{ id: 1, title: 'Item 1' }];

// BAD — using array index as ID (breaks on reorder)
const items = data.map((d, i) => ({ ...d, id: String(i) }));

// BAD — missing ID field
const items = [{ title: 'Item 1' }];
renderItem をメモ化する

不要な再レンダリングを避けるため、renderItem には必ず useCallback を使用してください。

// GOOD
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
), []);

// BAD — creates new function on every render
const renderItem = ({ item, ...props }) => (
  <SortableItem key={item.id} id={item.id} data={item} {...props}>
    <MyItemComponent item={item} />
  </SortableItem>
);
renderItem の props をスプレッドする

renderItem コールバックは、親の Sortable/SortableGrid から共有値と設定を受け取ります。必ずそれらを項目コンポーネントにスプレッドしてください。

// GOOD — spread all props from renderItem
const renderItem = useCallback(({ item, ...props }) => (
  <SortableItem id={item.id} data={item} {...props}>
    {/* content */}
  </SortableItem>
), []);

// BAD — manually passing individual props (easy to miss required ones)
const renderItem = useCallback(({ item, positions, lowerBound }) => (
  <SortableItem id={item.id} data={item} positions={positions} lowerBound={lowerBound}>
    {/* missing autoScrollDirection, itemsCount, etc. */}
  </SortableItem>
), []);
インタラクティブなコンテンツにはハンドルを使用する

ソート可能な項目にボタン、入力欄、その他のインタラクティブな要素が含まれる場合は、ドラッグとの競合を避けるためにハンドルを使用してください。

// GOOD — only the handle initiates drag, buttons work normally
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <SortableItem.Handle>
      <View style={styles.dragHandle} />
    </SortableItem.Handle>
    <Text>{item.title}</Text>
    <Button onPress={onDelete} title="Delete" />
  </View>
</SortableItem>

// PROBLEMATIC — entire item is draggable, button taps may trigger drag
<SortableItem id={item.id} data={item} {...props}>
  <View style={styles.row}>
    <Text>{item.title}</Text>
    <Button onPress={onDelete} title="Delete" />
  </View>
</SortableItem>
グリッドのアクティベーション遅延

項目をタップできるグリッドでは、タップとドラッグを区別するために activationDelay を使用してください。

<SortableGridItem
  id={item.id}
  data={item}
  activationDelay={250}
  {...props}
>
  <Pressable onPress={() => navigateTo(item)}>
    {/* Grid cell content */}
  </Pressable>
</SortableGridItem>
複雑なレイアウトには Hooks API を使用する

コンテナを完全に制御する必要がある場合(カスタムのスクロール動作、ネストされたリスト、標準外のレイアウトなど)は、コンポーネント API ではなく Hooks API を使用してください。

// Hooks give you raw shared values and gesture handlers
const { positions, scrollY, autoScroll, scrollViewRef, handleScroll, contentHeight, getItemProps } =
  useSortableList({ data, itemHeight: 60 });

// You control the ScrollView entirely
<Animated.ScrollView
  ref={scrollViewRef}
  onScroll={handleScroll}
  contentContainerStyle={{ height: contentHeight }}
  // Add your own custom scroll props
  showsVerticalScrollIndicator={false}
  bounces={false}
>
  {data.map((item, index) => (
    <CustomItem key={item.id} item={item} {...getItemProps(item, index)} />
  ))}
</Animated.ScrollView>
Sortable のネストを避ける

Sortable を別の Sortable または SortableGrid の内側にネストしないでください。それぞれが独自の DropProvider と GestureHandlerRootView を作成するため、ネストされたジェスチャーハンドラーが競合します。ネストされた並べ替え可能なリストが必要な場合は、単一の共有ジェスチャールートで Hooks API を使用してください。

コメント