react-native-drax

repository·main·Indexed 20 days ago

https://github.com/nuclearpasta/react-native-drax

A declarative drag-and-drop framework for React Native built with TypeScript. It provides high-performance, UI-thread-first interactions for free-form drag-and-drop, sortable lists, and cross-container reordering. The library includes components like DraxView, DraxList, and DraxHandle, and is compatible with the React Native New Architecture (Fabric). It requires react-native-reanimated and react-native-gesture-handler as peer dependencies.

Tokens
60.6K
Snippets
180
Records
257
Agent score
71%

What's inside react-native-drax

  1. How SortableBoardContainer coordinates transfers

    main

    The SortableBoardContainer manages the cross-container lifecycle through these steps:

    1. Monitoring: It wraps a DraxView with monitoring to listen to all drag events occurring within the board.
    2. Context: It provides a SortableBoardContext, allowing child columns to auto-register themselves via useEffect.
    3. Boundary Detection: During onMonitorDragOver, it checks if the current hover position has crossed into a different column's boundary.
    4. Transfer Initiation: When a transfer is detected, it ejects the item from the source container and creates a phantom slot in the target container.
    5. Finalization: Upon onDrop or onEnd, it fires the onTransfer event, clears the internal state, and manages the transition visibility.
  2. Use DraxProvider to manage drag-and-drop contexts

    main

    Every drag-and-drop area requires a DraxProvider at the root of the component tree. The provider manages the view registry, maintains the spatial index for UI-thread hit-testing, and renders the HoverLayer (the floating copy of the dragged view). You can use multiple providers to create separate, isolated drag contexts.

    <DraxProvider>
      {/* All DraxViews must be inside a provider */}
    </DraxProvider>
  3. How DraxProvider and DraxView work together

    main

    Drax is built around two fundamental components that enable drag-and-drop functionality via a declarative approach:

    1. DraxProvider: This component must wrap your entire application (or the specific area where drag-and-drop interactions occur). It provides the necessary React context that allows all child DraxView components to communicate and handle drag events.
    2. DraxView: This is the primary building block. It augments a standard React Native View with the ability to be dragged, to receive other drags, or to monitor other drags.

    While Drax provides higher-level components like DraxList (a drag-reorderable FlatList), these are implemented using DraxView internally.

  4. Use the Composable API for custom sortable lists

    main

    For full control over layout and behavior, use the low-level primitives. This pattern works with any list component like FlatList, FlashList, LegendList, or ScrollView.

    Core Primitives

    • useSortableList: A hook that manages reorder state and provides the necessary props to wire up a list.
    • SortableContainer: A wrapper for the list component that handles monitoring and auto-scrolling.
    • SortableItem: A wrapper for individual cells that handles shift animations.

    Wiring Requirements

    To make a list sortable using this pattern, you must wire the following from the sortable object to your list component:

    • sortable.data: The current state of the data.
    • sortable.stableKeyExtractor: The key extractor for the list.
    • sortable.onScroll: The scroll handler.
    • sortable.onContentSizeChange: The content size change handler.
    • sortable.onReorder: The reorder callback to update your state.
    import { useState, useRef } from 'react';
    import { FlatList, Text, View, StyleSheet } from 'react-native';
    import {
      DraxProvider,
      useSortableList,
      SortableContainer,
      SortableItem,
    } from 'react-native-drax';
    
    function App() {
      const [items, setItems] = useState(['A', 'B', 'C', 'D', 'E']);
      const listRef = useRef<FlatList>(null);
    
      const sortable = useSortableList({
        data: items,
        keyExtractor: (item) => item,
        onReorder: ({ data }) => setItems(data),
      });
    
      return (
        <DraxProvider>
          <SortableContainer sortable={sortable} scrollRef={listRef}>
            <FlatList
              ref={listRef}
              data={sortable.data}
              keyExtractor={sortable.stableKeyExtractor}
              onScroll={sortable.onScroll}
              onContentSizeChange={sortable.onContentSizeChange}
              renderItem={({ item, index }) => (
                <SortableItem sortable={sortable} index={index}>
                  <View style={styles.item}>
                    <Text>{item}</Text>
                  </View>
                </SortableItem>
              )}
            />
          </SortableContainer>
        </DraxProvider>
      );
    }
    
    const styles = StyleSheet.create({
      item: {
        padding: 16,
        backgroundColor: '#eee',
        margin: 4,
        borderRadius: 8,
      },
    });
  5. How Drax achieves 60fps performance

    main

    Drax uses a UI-Thread-First Architecture to ensure smooth interactions:

    1. Spatial Index Worklet: View positions are stored in a SharedValue<SpatialEntry[]>. Hit-testing is performed via a worklet directly on the UI thread, avoiding expensive JS-thread round-trips during gestures.
    2. SharedValue Frequency Split: To minimize re-renders, Drax splits SharedValues by their update frequency:
      • Every frame: hoverPositionSV, dragAbsolutePositionSV (read only by HoverLayer and gesture worklet).
      • Per receiver change: draggedIdSV, receiverIdSV, dragPhaseSV (read by DraxView animated styles).
      • On layout: spatialIndexSV (read by gesture worklet).

    This architecture ensures that most DraxView instances only re-evaluate their animated styles a few times per drag, rather than 60 times per second.

  6. Provide visual feedback using hover styles and dynamic content

    main

    To improve user experience during drag-and-drop operations, you can use the following techniques:

    • hoverStyle: Apply visual changes (like opacity or scale) to a drop zone while a draggable item is hovering over it.
    • renderContent: Dynamically change the content of a component based on its current drag state (e.g., showing a different icon or text when an item is hovering over it).
    • Snap alignment: Configure items to snap to specific positions upon being dropped.
  7. Use auto-generated accessibility labels in SortableItem

    main

    By default, SortableItem automatically provides accessibility attributes for screen readers. This allows users to understand the item's position in a list and how to interact with it without any manual configuration.

    The following attributes are generated automatically:

    • accessibilityLabel: Formatted as "Item N of M" (e.g., "Item 3 of 10") to indicate the item's position.
    • accessibilityHint: Set to "Long press to drag and reorder" to describe the interaction.
    • accessibilityRole: Set to "adjustable" to indicate the element can be reordered.
    // These are generated automatically:
    <SortableItem sortable={sortable} index={2}>
      {/* accessibilityLabel="Item 3 of 10" */}
      {/* accessibilityHint="Long press to drag and reorder" */}
      {/* accessibilityRole="adjustable" */}
      <Text>Cherry</Text>
    </SortableItem>
  8. How DraxHandle works with DraxView

    main

    The interaction between DraxHandle and DraxView follows this lifecycle:

    1. Parent Configuration: The parent DraxView is configured with dragHandle={true}, which prevents it from attaching a gesture to its own GestureDetector.
    2. Context Provision: The parent DraxView provides its gesture logic via DraxHandleContext.
    3. Gesture Consumption: The DraxHandle consumes this context and wraps its children with a GestureDetector.
    4. Hover Layer: When rendered in the hover layer, DraxHandle renders as a plain Reanimated.View (no gesture is needed in that layer).
  9. Auto-scroll in DraxList and useSortableList

    main

    DraxList and SortableContainer manage auto-scrolling internally, so you do not need to wrap them in a DraxScrollView.

    If you are using the useSortableList hook, you can customize the auto-scroll thresholds by passing them into the hook's configuration object.

    // DraxList handles its own scrolling
    <DraxList
      data={items}
      keyExtractor={(item) => item}
      onReorder={({ data }) => setItems(data)}
      renderItem={({ item }) => <ItemCard item={item} />}
    />
    
    // Customizing thresholds with useSortableList
    const sortable = useSortableList({
      data: items,
      keyExtractor: (item) => item.id,
      onReorder: ({ data }) => setItems(data),
      autoScrollJumpRatio: 0.15,
      autoScrollBackThreshold: 0.1,
      autoScrollForwardThreshold: 0.9,
    });
  10. How SortableContainer manages reordering and scrolling

    main

    The SortableContainer works by wrapping a DraxView with isParent and monitoring enabled. It orchestrates the reordering process through the following lifecycle:

    1. Event Monitoring: It listens to onMonitorDragStart, onMonitorDragOver, onMonitorDragEnd, and onMonitorDragDrop.
    2. Slot Detection: It uses position-based slot detection (rather than receiver-based detection) to ensure stable reorder behavior.
    3. Auto-scroll: It automatically manages scrolling when a dragged item approaches the edges of the container.
    4. Finalization: The reorder is finalized only after the snap animation completes.
  11. How reduced motion support works in Drax

    main

    Drax provides built-in support for the device's reduced motion settings via Reanimated's useReducedMotion() hook. This behavior is automatically applied to SortableItem without requiring manual configuration.

    • When reduced motion is ON: All shift animations are skipped, and items snap instantly to their new positions.
    • When reduced motion is OFF: Animations play normally.