Neodrag v2 to v3

Neodrag v2 to v3

Migrating from Neodrag v2 to v3

This guide covers migrating from Neodrag v2’s options-based API to v3’s plugin-based architecture.

Why Migrate to v3?

v2 Limitations:

  • Monolithic bundle - pay for unused features
  • Performance issues - 3 event listeners per draggable
  • Limited extensibility - can’t add custom behavior
  • Manual reactivity management

v3 Improvements:

  • Tree-shakable plugins - 50% smaller bundle
  • Event delegation - 3 listeners total regardless of draggable count
  • Custom plugin support
  • Better performance and pointer capture

Architecture Overview

Aspectv2v3
ConfigurationSingle options objectPlugin array
ReactivityAutomaticManual compartments
EventsBuilt-in callbacksEvent plugin
BoundsString or objectBoundsFrom helpers
Bundle Size~2.2KB all features~1.1KB plus only used plugins

Installation

npm install @neodrag/react@next
// Old imports
import { useDraggable } from '@neodrag/react';

// New imports
import {
  useDraggable,
  axis,
  bounds,
  BoundsFrom,
  events,
} from '@neodrag/react';

Basic Usage Examples

Before - v2 Options:

import { useRef } from 'react';
import { useDraggable } from '@neodrag/react';

function App() {
  const ref = useRef(null);

  useDraggable(ref, {
    axis: 'x',
    bounds: 'parent',
    grid: [10, 10],
    onDrag: (data) => console.log(data),
  });

  return <div ref={ref}>Drag me</div>;
}

After - v3 Plugins:

import { useRef } from 'react';
import {
  useDraggable,
  axis,
  bounds,
  BoundsFrom,
  grid,
  events,
} from '@neodrag/react';

function App() {
  const ref = useRef(null);

  useDraggable(ref, [
    axis('x'),
    bounds(BoundsFrom.parent()),
    grid([10, 10]),
    events({ onDrag: (data) => console.log(data) }),
  ]);

  return <div ref={ref}>Drag me</div>;
}

Plugin Conversion Reference

Movement Control

v2 Optionv3 PluginPurpose
axis: 'x'axis('x')Horizontal only
axis: 'y'axis('y')Vertical only
axis: 'both'no plugin neededBoth directions (default)
disabled: truedisabled()Disable dragging

Bounds and Constraints

v2 Optionv3 PluginPurpose
bounds: 'parent'bounds(BoundsFrom.parent())Constrain to parent element
bounds: '#container'bounds(BoundsFrom.selector('#container'))Constrain to specific element
bounds: elementbounds(BoundsFrom.element(element))Constrain to DOM element
bounds: { top: 50 }bounds(BoundsFrom.viewport({ top: 50 }))Constrain to viewport with padding

Grid and Snapping

v2 Optionv3 PluginPurpose
grid: [20, 20]grid([20, 20])Snap to grid
threshold: { distance: 5 }threshold({ distance: 5 })Movement threshold
threshold: { delay: 100 }threshold({ delay: 100 })Time delay threshold

Controls and Interaction

v2 Optionv3 PluginPurpose
handle: '.handle'controls({ allow: ControlFrom.selector('.handle') })Drag only from handle
cancel: '.cancel'controls({ block: ControlFrom.selector('.cancel') })Prevent drag from area
ignoreMultitouch: trueignoreMultitouch(true)Ignore multiple touches

Events and Callbacks

Events moved from options to dedicated plugin. Event data structure changed:

  • v2: offsetX, offsetY, rootNode, currentNode, event
  • v3: offset object with x and y properties, rootNode, currentNode, event

v2 Events:

useDraggable(ref, {
  onDragStart: ({ offsetX, offsetY }) =>
    console.log('start', offsetX, offsetY),
  onDrag: ({ offsetX, offsetY }) =>
    console.log('drag', offsetX, offsetY),
  onDragEnd: ({ offsetX, offsetY }) =>
    console.log('end', offsetX, offsetY),
});

v3 Events:

useDraggable(ref, [
  events({
    onDragStart: ({ offset }) =>
      console.log('start', offset.x, offset.y),
    onDrag: ({ offset }) => console.log('drag', offset.x, offset.y),
    onDragEnd: ({ offset }) => console.log('end', offset.x, offset.y),
  }),
]);

Reactive Updates: The Biggest Change

v2 automatically updated when options changed. v3 requires manual compartments for reactive behavior.

v2 Automatic Reactivity:

function App() {
  const [currentAxis, setCurrentAxis] = useState('x');
  const [isConstrained, setIsConstrained] = useState(false);

  useDraggable(ref, {
    axis: currentAxis,
    bounds: isConstrained ? 'parent' : undefined,
  });

  return (
    <div>
      <div ref={ref}>Drag me</div>
      <button onClick={() => setCurrentAxis('y')}>Switch to Y</button>
      <button onClick={() => setIsConstrained(!isConstrained)}>
        Toggle bounds
      </button>
    </div>
  );
}

v3 Manual Compartments:

import { useCompartment } from '@neodrag/react';

function App() {
  const [currentAxis, setCurrentAxis] = useState('x');
  const [isConstrained, setIsConstrained] = useState(false);

  const axisComp = useCompartment(
    () => (currentAxis === 'both' ? null : axis(currentAxis)),
    [currentAxis],
  );
  const boundsComp = useCompartment(
    () => (isConstrained ? bounds(BoundsFrom.parent()) : null),
    [isConstrained],
  );

  useDraggable(ref, () => [axisComp, boundsComp].filter(Boolean));

  return (
    <div>
      <div ref={ref}>Drag me</div>
      <button onClick={() => setCurrentAxis('y')}>Switch to Y</button>
      <button onClick={() => setIsConstrained(!isConstrained)}>
        Toggle bounds
      </button>
    </div>
  );
}

New v3 Features

Custom Plugins

Create your own dragging behavior:

import { useDraggable, unstable_definePlugin } from '@neodrag/react';

const loggerPlugin = unstable_definePlugin(() => ({
  name: 'logger',
  setup(ctx) {
    console.log('Drag initialized');
    return { startTime: 0 };
  },
  start(ctx, state, event) {
    state.startTime = Date.now();
    console.log('Drag started');
  },
  drag(ctx, state, event) {
    const duration = Date.now() - state.startTime;
    console.log(`Dragging for ${duration}ms`);
  },
  end(ctx, state, event) {
    console.log('Drag ended');
  },
}));

function CustomExample() {
  const ref = useRef(null);
  useDraggable(ref, [loggerPlugin]);
  return <div ref={ref}>Custom behavior</div>;
}

ScrollLock Plugin

Prevent page scrolling during drag:

import { useDraggable, scrollLock } from '@neodrag/react';

function ScrollLockExample() {
  const ref = useRef(null);

  useDraggable(ref, [
    scrollLock({
      lockAxis: 'both',
      allowScrollbar: false,
    }),
  ]);

  return <div ref={ref}>No page scroll while dragging</div>;
}

Framework-Specific Syntax Changes

Key React Changes:

  • Same useDraggable hook pattern
  • Options object becomes plugin array
  • Add useCompartment for reactive plugins with dependency arrays
  • No other major syntax changes

Migration Checklist

Step 1: Update Dependencies

  • Install v3 package for your framework
  • Update import statements to include needed plugins
  • Update TypeScript types if using TypeScript

Step 2: Convert Options to Plugins

  • Replace options object with plugin array
  • Convert each option using the reference tables above
  • Import required plugin functions

Step 3: Handle Reactive Updates

  • Identify dynamic/reactive options in v2 code
  • Create compartments for each reactive value
  • Wire up compartment updates to state changes

Step 4: Update Framework Syntax

  • Apply framework-specific syntax changes
  • Update event handlers to use events plugin
  • Test basic functionality

Step 5: Update Event Data Usage

  • Change from offsetX/offsetY to offset.x/offset.y
  • Update any event handler logic
  • Verify all event callbacks work correctly

Step 6: Test and Validate

  • Test all drag behaviors
  • Verify reactive updates work with compartments
  • Check performance and bundle size improvements
  • Test on different devices and browsers

Common Migration Issues

Missing Plugin Imports Make sure to import all plugins you use. Forgetting imports is the most common issue.

Reactivity Not Working Remember to use compartments for any value that should update dynamically. v3 doesn’t have automatic reactivity like v2.

Events Not Firing Events are now handled by the events plugin, not built-in options. Add the events plugin with your callbacks.

TypeScript Errors Update type imports and ensure you’re using the correct v3 types for your framework.

Performance Benefits

Memory Usage: Event delegation means 3 total listeners instead of 3 per draggable
Reliability: Pointer capture prevents losing drag when moving over iframes
Extensibility: Custom plugins allow community-driven extensions

The migration requires some refactoring, but v3 provides significant improvements in performance, bundle size, and flexibility. Take it step-by-step, use compartments for reactivity, and enjoy the enhanced capabilities of Neodrag v3!