Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pan Responder only fires once when updating parent component's state?

Tags:

I'm using React Native's Pan Responder. I need to update some state in a parent when a Pan Responder in it's children is dragged.

The complex bit is that I also need these children to insert their own element into the draggable area.

For context, here is simpler example that works fine https://snack.expo.io/@jamesweblondon/drag-items

import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';

const items = ['1', '2', '3'];
const ITEM_HEIGHT = 100;

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);
  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View
        style={{ height: ITEM_HEIGHT * items.length, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
          const isBeingDragged = itemIndex === index;
          const top =
            isBeingDragged
              ? (ITEM_HEIGHT * itemIndex) + y
              : (ITEM_HEIGHT * itemIndex);
          return (
            <View
              style={{
                top,
                width: '100%',
                position: 'absolute',
                zIndex: isBeingDragged ? 1 : 0
              }}
              key={itemIndex}>
              <Child
                index={itemIndex}
                setIndex={setIndex}
                setY={setY}
                item={item}
              />
            </View>
          );
        })}
      </View>
    </View>
  );
};

const Child = ({ index, setIndex, setY, item }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        setY(gestureState.dy);
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        setY(0);
        setIndex(null);
      },
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;

  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'tomato',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: ITEM_HEIGHT,
      }}>
      <View
        {...panResponder.panHandlers}
        style={{ background: 'grey', height: '100%', width: 40 }}
      />
      <Text>Child {item}</Text>
    </View>
  );
};

export default Parent;

enter image description here

Here is my full example: https://snack.expo.io/@jamesweblondon/drag2

import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';

const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;

const items = [
  { type: CHILD_A, text: '1' },
  { type: CHILD_B, text: '2' },
  { type: CHILD_A, text: '3' },
];

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);

  const heights = items.map((item) =>
    item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
  );

  let heightsSum = 0;
  const heightsCumulative = heights.map(
    (elem) => (heightsSum = heightsSum + elem)
  );

  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View style={{ height: heightsSum, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
          if (item.type === CHILD_A) {
            return (
              <ChildA
                index={itemIndex}
                setIndex={setIndex}
                setY={setY}
                text={item.text}
                DragHandle={(props) => (
                  <DragHandle
                    {...props}
                    index={itemIndex}
                    setIndex={setIndex}
                    setY={setY}
                  />
                )}
              />
            );
          }
          return (
            <ChildB
              index={itemIndex}
              setIndex={setIndex}
              setY={setY}
              text={item.text}
              DragHandle={(props) => (
                <DragHandle
                  {...props}
                  index={itemIndex}
                  setIndex={setIndex}
                  setY={setY}
                />
              )}
            />
          );
        })}
      </View>
    </View>
  );
};

const DragHandle = ({ index, setIndex, setY, children }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        console.log(gestureState.dy); // This works when the line below is removed :)
        setY(gestureState); // This does not work :(
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {},
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      {...panResponder.panHandlers}
      style={{ background: 'grey', height: '100%', width: 40, padding: 10 }}>
      {children}
    </View>
  );
};

const ChildA = ({ index, setIndex, setY, text, DragHandle }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'gold',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: CHILD_A_HEIGHT,
      }}>
      <DragHandle>
        <View
          style={{ backgroundColor: 'goldenrod', width: '100%', height: '100%' }}
        />
      </DragHandle>
      <Text>Child A: {text}</Text>
    </View>
  );
};

const ChildB = ({ index, setIndex, setY, text, DragHandle }) => {
  return (
    <View
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        backgroundColor: 'green',
        padding: 10,
        borderBottomColor: 'black',
        borderBottomWidth: 1,
        height: CHILD_B_HEIGHT,
      }}>
      <DragHandle>
      <View
          style={{ backgroundColor: 'lawngreen', width: '100%', height: '100%' }}
        />
      </DragHandle>
      <Text>Child B: {text}</Text>
    </View>
  );
};

export default Parent;

When you drag on the DragHandle component these 2 functions fire once initially but don't again as you continue to drag:

console.log(gestureState.dy); // This works when the line below is removed :)
setY(gestureState); // This does not work :(

If I comment out this line: setY(gestureState); // This does not work :(

Then the console.log above it does work. It continues to log many times per second as you drag over it: console.log(gestureState.dy); // This works when the line below is removed :)

I therefore think it's to do with the Pan Responder being recreated when the parent's state changes but I'm not sure how to fix it. I'm also not sure why the simpler example doesn't have this issue.

like image 289
Evanss Avatar asked Jan 03 '21 15:01

Evanss


1 Answers

import React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';

const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;

const _items = [
  { type: CHILD_A, height: CHILD_A_HEIGHT, text: 'A' },
  { type: CHILD_B, height: CHILD_B_HEIGHT, text: 'B' },
  { type: CHILD_A, height: CHILD_A_HEIGHT, text: 'C' },
];

const Parent = () => {
  const [y, setY] = useState(0);
  const [index, setIndex] = useState(null);
  const [items, setItems] = useState(_items);

  const heights = items.map((item) =>
    item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
  );

  let heightsSum = 0;
  const heightsCumulative = heights.map(
    (elem) => (heightsSum = heightsSum + elem)
  );

  function setPosition(index, position) {
    setItems(items => {
      if (!items[index].hasOwnProperty('position')) {
        items[index]['position'] = position;
      }
      return items;
    })
  }

  return (
    <View style={{ marginTop: 50 }}>
      <Text>Index: {index}</Text>
      <Text>Y: {y}</Text>
      <View style={{ height: heightsSum, backgroundColor: 'gold' }}>
        {items.map((item, itemIndex) => {
          const isBeingDragged = itemIndex === index;

          const top = isBeingDragged
            ? item.position + y
            : item.position;

          const childProps = {
              top,
              setPosition,
              index: itemIndex,
              setIndex: setIndex,
              setY: setY,
              text: item.text,
              isBeingDragged: isBeingDragged
          };

          if (item.type === CHILD_A) {
            return (
              <ChildA
                 {...childProps}
              />
            );
          }
          return (
            <ChildB
                {...childProps}
            />
          );
        })}
      </View>
    </View>
  );
};

const DragHandle = ({ index, setIndex, setY, children }) => {
  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => true,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
      onMoveShouldSetPanResponder: (evt, gestureState) => true,
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
      onPanResponderGrant: (evt, gestureState) => {
        setIndex(index);
      },
      onPanResponderMove: (evt, gestureState) => {
        setY(gestureState.dy);
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        setY(0);
        setIndex(null);
      },
      onPanResponderTerminate: (evt, gestureState) => {},
      onShouldBlockNativeResponder: (evt, gestureState) => true,
    })
  ).current;
  return (
    <View
      {...panResponder.panHandlers}
      style={{ background: 'grey', width: 40, padding: 10 }}>
      {children}
    </View>
  );
};

const ChildA = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
  return (
    <View
      style={{
        top,
        width: '100%',
        position: top == undefined ? 'relative' : 'absolute',
        zIndex: isBeingDragged ? 1 : 0,
      }}
      onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
      key={index}>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          backgroundColor: 'gold',
          padding: 10,
          borderBottomColor: 'black',
          borderBottomWidth: 1,
          height: CHILD_A_HEIGHT,
        }}>
        <DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
          <View
            style={{
              backgroundColor: 'goldenrod',
              width: '100%',
              height: '100%',
            }}
          />
        </DragHandle>
        <Text>Child A: {text}</Text>
      </View>
    </View>
  );
};

const ChildB = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
  return (
    <View
      style={{
        top,
        width: '100%',
        position: top == undefined ? 'relative' : 'absolute',
        zIndex: isBeingDragged ? 1 : 0,
      }}
      onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
      key={index}>
      <View
        style={{
          flexDirection: 'row',
          justifyContent: 'space-between',
          backgroundColor: 'green',
          padding: 10,
          borderBottomColor: 'black',
          borderBottomWidth: 1,
          height: CHILD_B_HEIGHT,
        }}>
        <DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
          <View
            style={{
              backgroundColor: 'lawngreen',
              width: '100%',
              height: '100%',
            }}
          />
        </DragHandle>
        <Text>Child B: {text}</Text>
      </View>
    </View>
  );
};

export default Parent;

Note: ChildA and ChildB components can also be reduced to single component

Here working example

like image 130
Chandan Avatar answered Nov 15 '22 05:11

Chandan