|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +description: 'Learn how to create custom sorting strategies' |
| 4 | +title: Custom Strategy |
| 5 | +--- |
| 6 | + |
| 7 | +import Tabs from '@theme/Tabs'; |
| 8 | +import TabItem from '@theme/TabItem'; |
| 9 | + |
| 10 | +# Custom Ordering Strategy |
| 11 | + |
| 12 | +While `react-native-sortables` comes with built-in strategies for Grid and Flex layouts, you might need specific behavior that isn't covered by the defaults. In such cases, you can implement a **Custom Sort Strategy**. |
| 13 | + |
| 14 | +:::info |
| 15 | + |
| 16 | +This guide describes advanced customization. Familiarity with [Reanimated worklets](https://docs.swmansion.com/react-native-reanimated/docs/2.x/worklets/) is recommended as strategies run on the UI thread. |
| 17 | + |
| 18 | +::: |
| 19 | + |
| 20 | +## What is a Strategy? |
| 21 | + |
| 22 | +A strategy is a function that determines the **new order of items** based on the **active item's position**. It is called repeatedly while the user drags an item. |
| 23 | + |
| 24 | +Technically, a strategy is defined by a **Factory Function** (`SortStrategyFactory`) which returns an **Updater Function** (`OrderUpdater`). |
| 25 | + |
| 26 | +### `SortStrategyFactory` |
| 27 | + |
| 28 | +This is a **React Hook** that runs on the JS thread. It is called once when the component mounts or when dependencies change. Its primary purpose is to **prepare data** (like layout values, item sizes) needed for the updater function. |
| 29 | + |
| 30 | +```ts |
| 31 | +type SortStrategyFactory = () => OrderUpdater; |
| 32 | +``` |
| 33 | + |
| 34 | +### `OrderUpdater` |
| 35 | + |
| 36 | +This is a **Worklet Function** that runs on the UI thread. It is called on every frame during the drag gesture. |
| 37 | + |
| 38 | +```ts |
| 39 | +type OrderUpdater = (params: { |
| 40 | + activeKey: string; |
| 41 | + activeIndex: number; |
| 42 | + dimensions: { width: number; height: number }; |
| 43 | + position: { x: number; y: number }; |
| 44 | +}) => Array<string> | undefined | null; |
| 45 | +``` |
| 46 | + |
| 47 | +- **Returns**: A new array of item keys representing the new order, or `undefined`/`null` if the order hasn't changed. |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Tutorial: Simple Grid Strategy |
| 52 | + |
| 53 | +Let's build a simplified strategy for a **Grid** layout where all items have the **same size**. We will implement a basic "swap" behavior: when an item is dragged over another item, they swap places. |
| 54 | + |
| 55 | +### Step 1: Access Contexts |
| 56 | + |
| 57 | +First, we need to access the layout information. `react-native-sortables` exposes several hooks for this: |
| 58 | + |
| 59 | +- `useCommonValuesContext()`: Provides shared values like `indexToKey` (current order), `containerWidth`, `itemWidths`, etc. |
| 60 | +- `useGridLayoutContext()`: Specific to Grid, provides `columns`, `rows`, gaps, etc. |
| 61 | + |
| 62 | +### Step 2: Implement the Factory |
| 63 | + |
| 64 | +We create a factory hook that prepares the shared values. |
| 65 | + |
| 66 | +```tsx |
| 67 | +import { useCommonValuesContext } from 'react-native-sortables'; |
| 68 | +import { useGridLayoutContext } from 'react-native-sortables'; |
| 69 | + |
| 70 | +const useSimpleGridStrategy = () => { |
| 71 | + // 1. Get shared values from context |
| 72 | + const { indexToKey, itemWidths, itemHeights } = useCommonValuesContext(); |
| 73 | + const { columns, isVertical } = useGridLayoutContext(); |
| 74 | + |
| 75 | + // 2. Return the worklet function |
| 76 | + return params => { |
| 77 | + 'worklet'; |
| 78 | + // Implementation will go here |
| 79 | + return undefined; |
| 80 | + }; |
| 81 | +}; |
| 82 | +``` |
| 83 | + |
| 84 | +### Step 3: Implement the Worklet |
| 85 | + |
| 86 | +Inside the worklet, we calculate which index the dragged item is hovering over, and if it's different from the current index, we swap them. |
| 87 | + |
| 88 | +```tsx |
| 89 | +// Helper to swap items in array |
| 90 | +function swap(array: string[], from: number, to: number) { |
| 91 | + 'worklet'; |
| 92 | + const newArray = [...array]; |
| 93 | + [newArray[from], newArray[to]] = [newArray[to], newArray[from]]; |
| 94 | + return newArray; |
| 95 | +} |
| 96 | + |
| 97 | +// ... inside the returned function: |
| 98 | +return ({ activeIndex, position }) => { |
| 99 | + 'worklet'; |
| 100 | + |
| 101 | + // 1. Calculate the target index based on position |
| 102 | + // (Simplified for this tutorial with constants) |
| 103 | + const COLUMNS = 3; |
| 104 | + const ITEM_WIDTH = 100; |
| 105 | + const ITEM_HEIGHT = 100; |
| 106 | + const GAP = 10; |
| 107 | + |
| 108 | + // Approximate row and column from position |
| 109 | + const col = Math.floor( |
| 110 | + Math.max(0, position.x + ITEM_WIDTH / 2) / (ITEM_WIDTH + GAP) |
| 111 | + ); |
| 112 | + const row = Math.floor( |
| 113 | + Math.max(0, position.y + ITEM_HEIGHT / 2) / (ITEM_HEIGHT + GAP) |
| 114 | + ); |
| 115 | + |
| 116 | + const targetIndex = row * COLUMNS + col; |
| 117 | + const currentOrder = indexToKey.value; |
| 118 | + |
| 119 | + // boundary check |
| 120 | + if (targetIndex < 0 || targetIndex >= currentOrder.length) return; |
| 121 | + |
| 122 | + // 2. If index changed, return new order |
| 123 | + if (targetIndex !== activeIndex) { |
| 124 | + return swap(currentOrder, activeIndex, targetIndex); |
| 125 | + } |
| 126 | + |
| 127 | + return undefined; // No change |
| 128 | +}; |
| 129 | +``` |
| 130 | + |
| 131 | +### Full Example |
| 132 | + |
| 133 | +Here is the complete code for a simple strategy that assumes a fixed 3-column grid. |
| 134 | + |
| 135 | +```tsx |
| 136 | +import { useCallback } from 'react'; |
| 137 | +import { |
| 138 | + useCommonValuesContext, |
| 139 | + SortStrategyFactory |
| 140 | +} from 'react-native-sortables'; |
| 141 | + |
| 142 | +// Simple reorder helper |
| 143 | +function swap(array: string[], from: number, to: number) { |
| 144 | + 'worklet'; |
| 145 | + const newArray = [...array]; |
| 146 | + [newArray[from], newArray[to]] = [newArray[to], newArray[from]]; |
| 147 | + return newArray; |
| 148 | +} |
| 149 | + |
| 150 | +export const useSimpleSwapStrategy: SortStrategyFactory = () => { |
| 151 | + const { indexToKey } = useCommonValuesContext(); |
| 152 | + |
| 153 | + return ({ activeIndex, position, dimensions }) => { |
| 154 | + 'worklet'; |
| 155 | + |
| 156 | + const COLUMNS = 3; |
| 157 | + const ITEM_WIDTH = 100; |
| 158 | + const ITEM_HEIGHT = 100; |
| 159 | + const GAP = 10; |
| 160 | + |
| 161 | + // Calculate index based on center position of the dragged item |
| 162 | + const centerX = position.x + dimensions.width / 2; |
| 163 | + const centerY = position.y + dimensions.height / 2; |
| 164 | + |
| 165 | + const col = Math.floor(centerX / (ITEM_WIDTH + GAP)); |
| 166 | + const row = Math.floor(centerY / (ITEM_HEIGHT + GAP)); |
| 167 | + |
| 168 | + const targetIndex = row * COLUMNS + col; |
| 169 | + const currentOrder = indexToKey.value; |
| 170 | + |
| 171 | + if ( |
| 172 | + targetIndex >= 0 && |
| 173 | + targetIndex < currentOrder.length && |
| 174 | + targetIndex !== activeIndex |
| 175 | + ) { |
| 176 | + return swap(currentOrder, activeIndex, targetIndex); |
| 177 | + } |
| 178 | + |
| 179 | + return undefined; |
| 180 | + }; |
| 181 | +}; |
| 182 | + |
| 183 | +// Usage |
| 184 | +// <Sortable.Grid strategy={useSimpleSwapStrategy} ... /> |
| 185 | +``` |
| 186 | + |
| 187 | +### Usage Example |
| 188 | + |
| 189 | +Here is how you can use the custom strategy in a component. |
| 190 | + |
| 191 | +```tsx |
| 192 | +import React, { useCallback } from 'react'; |
| 193 | +import { StyleSheet, Text, View } from 'react-native'; |
| 194 | +import Sortable, { SortableGridRenderItem } from 'react-native-sortables'; |
| 195 | +import { useSimpleSwapStrategy } from './useSimpleSwapStrategy'; // inner file import |
| 196 | + |
| 197 | +const DATA = Array.from({ length: 9 }, (_, i) => `Item ${i + 1}`); |
| 198 | +const COLUMNS = 3; |
| 199 | + |
| 200 | +export default function App() { |
| 201 | + const renderItem = useCallback<SortableGridRenderItem<string>>( |
| 202 | + ({ item }) => ( |
| 203 | + <View style={styles.card}> |
| 204 | + <Text style={styles.text}>{item}</Text> |
| 205 | + </View> |
| 206 | + ), |
| 207 | + [] |
| 208 | + ); |
| 209 | + |
| 210 | + return ( |
| 211 | + <View style={styles.container}> |
| 212 | + <Sortable.Grid |
| 213 | + data={DATA} |
| 214 | + renderItem={renderItem} |
| 215 | + columns={COLUMNS} |
| 216 | + rowHeight={100} |
| 217 | + columnGap={10} |
| 218 | + rowGap={10} |
| 219 | + strategy={useSimpleSwapStrategy} |
| 220 | + /> |
| 221 | + </View> |
| 222 | + ); |
| 223 | +} |
| 224 | + |
| 225 | +const styles = StyleSheet.create({ |
| 226 | + container: { |
| 227 | + flex: 1, |
| 228 | + padding: 20, |
| 229 | + justifyContent: 'center', |
| 230 | + backgroundColor: '#f5f5f5' |
| 231 | + }, |
| 232 | + card: { |
| 233 | + flex: 1, |
| 234 | + height: 100, // Matches ITEM_HEIGHT in strategy |
| 235 | + justifyContent: 'center', |
| 236 | + alignItems: 'center', |
| 237 | + backgroundColor: 'white', |
| 238 | + borderRadius: 8, |
| 239 | + borderWidth: 1, |
| 240 | + borderColor: '#ddd' |
| 241 | + }, |
| 242 | + text: { |
| 243 | + fontWeight: 'bold', |
| 244 | + fontSize: 16 |
| 245 | + } |
| 246 | +}); |
| 247 | +``` |
| 248 | + |
| 249 | +<div style={{ textAlign: 'center', margin: '20px 0' }}> |
| 250 | + {/* <video src={require('@site/static/video/custom-strategy-demo.mp4').default} autoPlay loop muted playsInline style={{ maxWidth: '100%', borderRadius: 8 }} /> */} |
| 251 | + <div |
| 252 | + style={{ |
| 253 | + padding: '40px', |
| 254 | + backgroundColor: '#f0f0f0', |
| 255 | + borderRadius: '8px', |
| 256 | + border: '2px dashed #ccc', |
| 257 | + color: '#666' |
| 258 | + }}> |
| 259 | + Video Demo Placeholder |
| 260 | + </div> |
| 261 | +</div> |
| 262 | + |
| 263 | +## Available Contexts |
| 264 | + |
| 265 | +When building specific strategies, you can access detailed state from these contexts: |
| 266 | + |
| 267 | +### `useCommonValuesContext` |
| 268 | + |
| 269 | +- `indexToKey`: Current order of items. |
| 270 | +- `containerWidth` / `containerHeight`: Dimensions of the sortable container. |
| 271 | +- `itemWidths` / `itemHeights`: Dimensions of individual items. |
| 272 | +- `activeItemKey`: Key of the item currently being dragged. |
| 273 | + |
| 274 | +### `useGridLayoutContext` (Grid Only) |
| 275 | + |
| 276 | +- `columns` / `rows`: Number of columns/rows. |
| 277 | +- `gap` / `rowGap` / `columnGap`: Spacing configuration. |
| 278 | +- `isVertical`: Orientation of the grid. |
| 279 | + |
| 280 | +### `useFlexLayoutContext` (Flex Only) |
| 281 | + |
| 282 | +- `flexDirection`: Layout direction. |
| 283 | +- `flexWrap`: Wrap behavior. |
| 284 | + |
| 285 | +:::tip |
| 286 | + |
| 287 | +Always use the `'worklet'` directive in your updater function and helper functions to ensure they run smoothly on the UI thread. |
| 288 | + |
| 289 | +::: |
0 commit comments