SHISME / react-native-draggable-grid

A draggable and sortable grid of react-native
308 stars 96 forks source link

DraggableGrid breaks `InteractionManager.runAfterInteractions` callbacks #83

Open michalziolkowski opened 1 year ago

michalziolkowski commented 1 year ago

In our app, we use a lot of InteractionManager.runAfterInteractions callbacks, but the way react-native-draggable-grid is implemented right now can lead to that callback being stuck and not responding at all until the app is restarted.

The problem appears when DraggableGrid gets re-rendered when you are in the middle of dragging an item. In our specific case, this happens because whenever an item is being dragged we want to change how it looks in that state so we update the component to trigger renderItem with the new state.

Why InteractionManager.runAfterInteractions may break:

During onPanResponderGrant, a PanResponder makes a call to createInteractionHandle which effectively blocks all calls to runAfterInteractions until the handle has been cleared. The value of the handle is stored in the PanResponder's local state, and when PanResponder terminates, if the handle is found to be !== null, it is cleared and the pending tasks are flushed. As discovered in an issue from different library here

Why it breaks forDraggableGrid:

The problem is that when we re-render the DraggableGrid it never terminates properly because PanResponder is initialized with each render so previous instance never terminates.

How this can be solved:

A solution to this would be to initialize PanResponder as it is suggested in react-native docs here, using the useRef, but simply doing that breaks how the component works because we rely on panResponderCapture state and also some props like onDragStart, onDragging, onResetSort and onDragRelease may be updated and this will not be reflected in PanResponder callbacks, because these would be initialized just once with useRef initialization.

Workaround:

As a workaround in our app we've just memoized the DraggableItem and pass data to rendered items through Context so it by-passes the DraggableItem re-rendering, but it feels like a hacky solution. So it should either be documented or there should come some design change to this library so it doesn't break InteractionManager.runAfterInteractions callbacks.

An example code of how we workaround the issue:

CustomGrid.tsx

export const DraggedItemContext = createContext<DraggableItemType | undefined>(
  undefined,
);

const CustomGrid = () => {
  const [currentDraggedItem, setCurrentDraggedItem] = useState();

  const renderItem = (item: DraggableItemType) =>   <CustomDraggableItem key={item.key} item={item} />

   const onDragStart = (item: DraggableItemType) => {
     setCurrentDraggedItem(item);
   };

  const draggableGrid = useMemo(() => {
      return (
        <DraggableGrid
          data={itemsData}
          renderItem={renderItem}
          onDragStart={onDragStart}
        />
      );
    }, [itemsData]);

  return (
      <DraggedItemContext.Provider value={currentDraggedItem}>
        {draggableGrid}
      </DraggedItemContext.Provider>
  };
};

CustomGrid.tsx

const CustomDraggableItem = ({ item }: CustomDraggableItemProps) => {
  const currentDraggedItem = useContext(DraggedItemContext);

  return (
      <CustomGridItem item={item} isBeingDragged={item.key  === currentDraggedItem?.key;} />
  };
};