rohan
rohan

Reputation: 127

Unexpected behaviour when React Native re-renders component

I'm using react-native-card-stack-swiper library in my react-native app. I'm using firebase realtime database for backend.

Expected behaviour:

this.state.peopleList is { personA, personB } this list is derived from firebase realtime database people/peopleList.

Here is Home.js

import React, {Component} from 'react';
import {FlatList, View, Text, ActivityIndicator, Alert, StyleSheet, TouchableOpacity, Dimensions} from 'react-native';
import { addToAccepted, getHiddenPosts} from "../lib/firebaseUtils";
import firebase from 'react-native-firebase';
import { Button, ListItem, Card, Icon as IconElements } from 'react-native-elements';
import ActionSheet from 'react-native-actionsheet'
import Icon from 'react-native-vector-icons/FontAwesome';
import MaterialComIcon from 'react-native-vector-icons/MaterialCommunityIcons';
import * as _ from 'lodash';
import CardStack, { Card as SwipableCard } from 'react-native-card-stack-swiper';
import OfflineNotice from './OfflineNotice';

let uid;

class HomeScreen extends Component {
    constructor(props) {
        super(props);
        this.state = {
            peopleList: [],
            hiddenPosts: [],
            fetching: false,
        };
        this.getPeopleList = this.getPeopleList.bind(this);
    }

    componentDidMount(){
        this._isMounted = true;
        this.setState({fetching:true});
        let user = firebase.auth().currentUser;
        if (user != null) {
          uid = user.uid;
        } else {
          this.props.navigation.navigate('Login')
        }

        getHiddenPosts(uid).then(hiddenPosts => {
          this.setState({hiddenPosts});
        })
    }

    componentWillUnmount()
    {
        this._isMounted = false;
    }

    /*
    * get all the task requests that this user can perform
    * */
    getPeopleList = () => {
        let networkId = this.state.networkId;
        let livePostsRef = firebase.database().ref('people/peopleList')
        livePostsRef.on('child_added', (snapshot) => {

          let request  = snapshot.val()
          // Check if it is not already decided upon by this user
          if(!_.includes(this.state.hiddenPosts, request.id))
          {
            this.setState({peopleList:[request].concat(this.state.peopleList) , fetching: false});
          }
          if(this.state.fetching) this.setState({fetching:false});
        })
        if(this._isMounted) this.setState({fetching:false});

        livePostsRef.on('child_removed', (snapshot) => {
          this.setState({peopleList: this.state.peopleList.filter(item => item.id !== snapshot.key)});
        })
    }

    // The user has decided on this card and hence add this card to the user's hidden tasks list so that the app won't show it again next time it launches
    decideOnPost = (id) =>
    {
        this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
        if(uid) appendHiddenPosts(uid, id);
    }

    acceptPerson = (item) =>
    {
        addToAccepted(item).then(res => {
            appendHiddenPosts(uid, id).then(finRes => {
                this.setState({peopleList: this.state.peopleList.filter(item => item.id !== id)});
            }
        }

    }


    swipableRender() {
      const {peopleList} = this.state;

      console.log('swipableRender: peopleList is ', peopleList)

      return peopleList.map((item) => {
        const {name, photo, bio, id} = item;
        console.log('swipableRender return: item.name is ', item.name)

        return (
          <SwipableCard key={id} onSwipedLeft={() => this.decideOnPost(id)} onSwipedRight={() => this.acceptPerson(item)}>
          <View>
          {console.log('swipableRender return return: customTitle is ', customTitle)}


            <Card image={{uri: bgImage}} featuredTitle={customTitle} featuredTitleStyle={adourStyle.listItemText} >


                <Text style={adourStyle.cardText}>{details}</Text>




              </Card>
              </View>
            </SwipableCard>
        )
      })

  }

    render() {
        const {fetching, peopleList} = this.state
        console.log('*** RENDERING *** peopleList: ', peopleList)
        return (
            <View style={styles.mainContainer}>
            <CardStack
                renderNoMoreCards={() => <View style={{marginTop: 50}}>
                                                  {fetching && <ActivityIndicator color={BRAND_COLOR_ONE} size={'large'}/>}
                                                  {!fetching &&
                                                    <View style={styles.cardOverText}>
                                                    <Text style={{marginBottom: 8}}>Check back later</Text>
                                                    </View>

                                                  }
                                                  </View>}
                disableBottomSwipe={true}
                disableTopSwipe={true}
                ref={swiper => {
                  this.swiper = swiper
                }}
              >
              {this.swipableRender()}
              </CardStack>
            </View>
        )
    }


}

export default HomeScreen;

The card stack is rendered as expected initially. In other words, when the app is launched, I see a stack of cards with personA card and personB card. These cards can be swiped as expected.

If I swipe all the cards out: personA and personB, I am left with the text 'Please check back later' as expected.

However, if I am on the screen and at the same time a new personC object is added to firebase realtime database people/peopleList, I expect firebase.database().ref().on listener will detect the change in the database, do a setState on the state peopleList and therefore re-render the component. As a result I expect to see personC appear on my screen.

However, in actuality, firebase listener detects the change in the database as expected then we do a setState to update the values of this.state.peopleList then React re-renders the component as expected (verified using console.log) * BUT I see personA on my screen which is very weird*

Additionally, if I am still on the screen and another object is added to the firebase database: personD, the same thing happens. Instead of seeing personD card, I see personB card.

As a user, I already swiped left personA and personB and was left with an empty screen. When new objects are added while I'm on the screen, instead of seeing the new objects I see the old objects again.

If I close the app fully and re-launch it, the correct behavior happens: I see personC and personD. Until I swipe left on both personC and personD and the problem begins again unless I re-launch the app.

I'm pretty certain the problem resides either in this Home.js file or the library react-native-card-stack-swiper. The library is not being supported by its developer anymore, so if there is any issues with the library, I'll have to fix it.

react-native-card-stack-swiper

Here is react-native-card-stack-swiper (you can also find the source at its GitHub page: https://github.com/lhandel/react-native-card-stack-swiper

This is Card.js (imported into my corde as SwipableCard)

import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
  View,
} from 'react-native';

const Card = ({ style, children }) => (
  <View style={style} >
    {children}
  </View>);

Card.propTypes = {
  children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,
  style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
  onSwipedLeft: PropTypes.func,
  onSwipedRight:PropTypes.func,
  onSwipedTop: PropTypes.func,
  onSwipedBottom: PropTypes.func,
  onSwiped: PropTypes.func,
}
Card.defaultProps = {
  style:{},
  onSwiped: () => {},
  onSwipedLeft: () => {},
  onSwipedRight: () => {},
  onSwipedTop: () => {},
  onSwipedBottom: () => {},
}

export default Card;

This is CardStack.js

import React, { Component } from 'react';
import PropTypes from 'prop-types'
import {
  StyleSheet,
  View,
  Animated,
  PanResponder,
  Dimensions,
  Text,
  Platform
} from 'react-native';

const { height, width } = Dimensions.get('window');

export default class CardStack extends Component {


  static distance(x, y) {
    const a = Math.abs(x);
    const b = Math.abs(y);
    const c = Math.sqrt((a * a) + (b * b));
    return c;
  }

  constructor(props) {
    super(props);
    this.state ={
      drag: new Animated.ValueXY({x: 0, y: 0}),
      dragDistance: new Animated.Value(0),
      sindex: 0, // index to the next card to be renderd mod card.length
      cardA: null,
      cardB: null,
      topCard: 'cardA',
      cards: [],
      touchStart: 0,
    };
    this.distance = this.constructor.distance;
  }


  componentWillMount() {
    this._panResponder = PanResponder.create({
      onStartShouldSetPanResponder: (evt, gestureState) => false,
      onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
      onMoveShouldSetPanResponder: (evt, gestureState) =>  {
        const isVerticalSwipe = Math.sqrt(
          Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
        )
        if (!this.props.verticalSwipe && isVerticalSwipe) {
          return false
        }
        return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
      }, //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
      onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
        const isVerticalSwipe = Math.sqrt(
          Math.pow(gestureState.dx, 2) < Math.pow(gestureState.dy, 2)
        )
        if (!this.props.verticalSwipe && isVerticalSwipe) {
          return false
        }
        return Math.sqrt(Math.pow(gestureState.dx, 2) + Math.pow(gestureState.dy, 2)) > 10
      },  //(parseInt(gestureState.dx) !== 0 && parseInt(gestureState.dy) !== 0),
      onPanResponderGrant: (evt, gestureState) => {
        this.props.onSwipeStart();
        this.setState({ touchStart: new Date().getTime() });
      },
      onPanResponderMove: (evt, gestureState) => {
        const { verticalSwipe, horizontalSwipe } = this.props;
        const { verticalThreshold, horizontalThreshold } = this.props
        const dragDistance = this.distance((horizontalSwipe) ? gestureState.dx : 0, (verticalSwipe) ? gestureState.dy : 0 );
        this.state.dragDistance.setValue(dragDistance);
        this.state.drag.setValue({x: (horizontalSwipe) ? gestureState.dx : 0, y: (verticalSwipe) ? gestureState.dy : 0});
      },
      onPanResponderTerminationRequest: (evt, gestureState) => true,
      onPanResponderRelease: (evt, gestureState) => {
        this.props.onSwipeEnd();
        const currentTime = new Date().getTime();
        const swipeDuration = currentTime-this.state.touchStart;
        const { sindex } = this.state;
        const { verticalThreshold,
                horizontalThreshold,
                disableTopSwipe,
                disableLeftSwipe,
                disableRightSwipe,
                disableBottomSwipe,
              } = this.props;


        if (((Math.abs(gestureState.dy) > verticalThreshold)  ||
            ( Math.abs(gestureState.dy) > verticalThreshold*0.8 &&
              swipeDuration < 150)
            ) && this.props.verticalSwipe)
        {

          const swipeDirection = (gestureState.dy < 0) ? height * -1 : height;
          if(swipeDirection < 0 && !disableTopSwipe)
          {

            this._nextCard('top', gestureState.dx, swipeDirection, this.props.duration);
          }
          else if (swipeDirection > 0 && !disableBottomSwipe)
          {
            this._nextCard('bottom', gestureState.dx, swipeDirection, this.props.duration);
          }
          else
          {
            this._resetCard();
          }
        }else if (((Math.abs(gestureState.dx) > horizontalThreshold) ||
                  (Math.abs(gestureState.dx) > horizontalThreshold*0.6 &&
                  swipeDuration < 150)
                ) && this.props.horizontalSwipe) {

          const swipeDirection = (gestureState.dx < 0) ? width * -1 : width;
          if (swipeDirection < 0 && !disableLeftSwipe)
          {
            this._nextCard('left', swipeDirection, gestureState.dy, this.props.duration);
          }
          else if(swipeDirection > 0 && !disableRightSwipe)
          {
            this._nextCard('right', swipeDirection, gestureState.dy, this.props.duration);
          }
          else
          {
            this._resetCard();
          }
        }
        else
        {
          this._resetCard();
        }
      },
      onPanResponderTerminate: (evt, gestureState) => {
      },
      onShouldBlockNativeResponder: (evt, gestureState) => {
        return true;
      },
    });
  }

  componentDidMount(){
    this.initDeck();
  }

  componentWillReceiveProps(nextProps){
    if (nextProps.children !== this.props.children) {
      this.setState({
        cards: nextProps.children,
        cardA: nextProps.children[(this.state.topCard=='cardA')? this.state.sindex-2 : this.state.sindex-1],
        cardB: nextProps.children[(this.state.topCard=='cardB')? this.state.sindex-2 : this.state.sindex-1]
      });
    }
  }

  initDeck() {
    // check if we only have 1 child
    if(typeof this.props.children !== 'undefined' && !Array.isArray(this.props.children)){
      this.setState({
        cards: [this.props.children],
        cardA: this.props.children,
        cardB: null,
        sindex: 2,
      });
    }else if(Array.isArray(this.props.children)){
      this.setState({
        cards: this.props.children,
        cardA: this.props.children[0],
        cardB: this.props.children[1],
        sindex: 2,
      });
    }
  }

  _resetCard(){

    Animated.timing(
      this.state.dragDistance,
      {
        toValue: 0,
        duration: this.props.duration,
      }
    ).start();
    Animated.spring(
      this.state.drag,
      {
        toValue: {x: 0, y: 0},
        duration: this.props.duration,
      }
    ).start();

  }


  goBackFromTop(){
    this._goBack('top');
  }

  goBackFromRight(){
    this._goBack('right');
  }

  goBackFromLeft(){
    this._goBack('left');
  }

  goBackFromBottom(){
    this._goBack('bottom');
  }

  mod(n, m) {
    return ((n % m) + m) % m;
  }

  _goBack(direction){
    const {cardA, cardB, cards, sindex, topCard} = this.state;

    if((sindex-3) < 0 && !this.props.loop) return;

    const previusCardIndex = this.mod(sindex-3, cards.length)
    let update = {};
    if(topCard === 'cardA'){
      update = {
        ...update,
        cardB: cards[previusCardIndex]

      }
    }else{
      update = {
        ...update,
        cardA: cards[previusCardIndex],
      }
    }

    this.setState({
      ...update,
      topCard: (topCard === 'cardA') ? 'cardB' : 'cardA',
      sindex: sindex-1
    }, () => {

      switch (direction) {
        case 'top':
          this.state.drag.setValue({x: 0, y: -height});
          this.state.dragDistance.setValue(height);
          break;
        case 'left':
          this.state.drag.setValue({x: -width, y: 0});
          this.state.dragDistance.setValue(width);
          break;
        case 'right':
          this.state.drag.setValue({x: width, y: 0});
          this.state.dragDistance.setValue(width);
          break;
        case 'bottom':
          this.state.drag.setValue({x: 0, y: height});
          this.state.dragDistance.setValue(width);
          break;
        default:

      }

      Animated.spring(
        this.state.dragDistance,
        {
          toValue: 0,
          duration: this.props.duration,
        }
      ).start();

      Animated.spring(
        this.state.drag,
        {
          toValue: {x: 0, y: 0},
          duration: this.props.duration,
        }
      ).start();
    })
  }



  swipeTop(duration){
    this._nextCard('top', 0, -height, duration);
  }

  swipeBottom(duration){
    this._nextCard('bottom', 0, height, duration);
  }

  swipeRight(duration){
    this._nextCard('right', width, 0, duration);
  }

  swipeLeft(duration){
    this._nextCard('left', -width, 0, duration);
  }

  _nextCard(direction, x, y, duration=400){
    const { verticalSwipe, horizontalSwipe, loop } = this.props;
    const { sindex, cards, topCard } = this.state;

    // index for the next card to be renderd
    const nextCard = (loop) ? (Math.abs(sindex) % cards.length) : sindex;

    // index of the swiped card
    const index = (loop) ? this.mod(nextCard-2, cards.length) : nextCard - 2;

    if (index === cards.length-1){
      this.props.onSwipedAll();
    }


    if((sindex-2 < cards.length) || (loop) ){
      Animated.spring(
        this.state.dragDistance,
        {
          toValue: 220,
          duration,
        }
      ).start();

      Animated.timing(
        this.state.drag,
        {
          toValue: { x: (horizontalSwipe) ? x : 0, y: (verticalSwipe) ? y : 0 },
          duration,
        }
      ).start(() => {

        const newTopCard =  (topCard === 'cardA') ? 'cardB' : 'cardA';

        let update = {};
        if(newTopCard === 'cardA') {
          update = {
            ...update,
            cardB: cards[nextCard]
          };
        }
        if(newTopCard === 'cardB') {
          update = {
            ...update,
            cardA: cards[nextCard],
          };
        }
        this.state.drag.setValue({x: 0, y: 0});
        this.state.dragDistance.setValue(0);
        this.setState({
          ...update,
          topCard: newTopCard,
          sindex: nextCard+1
        });

        this.props.onSwiped(index);
        switch (direction) {
          case 'left':
            this.props.onSwipedLeft(index);
            this.state.cards[index].props.onSwipedLeft();
            break;
          case 'right':
            this.props.onSwipedRight(index);
            this.state.cards[index].props.onSwipedRight();
            break;
          case 'top':
            this.props.onSwipedTop(index);
            this.state.cards[index].props.onSwipedTop();
            break;
          case 'bottom':
            this.props.onSwipedBottom(index);
            this.state.cards[index].props.onSwipedBottom();
            break;
          default:
        }
      });

    }
  }


  /**
   * @description CardB’s click feature is trigger the CardA on the card stack. (Solved on Android)
   * @see https://facebook.github.io/react-native/docs/view#pointerevents
   */
  _setPointerEvents(topCard, topCardName) {
    return { pointerEvents: topCard === topCardName ? "auto" : "none" }
  }

  render() {

    const { secondCardZoom } = this.props;
    const { drag, dragDistance, cardA, cardB, topCard, sindex } = this.state;

    const SC = dragDistance.interpolate({
      inputRange: [0,10, 220],
      outputRange: [secondCardZoom,secondCardZoom,1],
      extrapolate: 'clamp',
    });
    const rotate = drag.x.interpolate({
      inputRange: [-320,0,320],
      outputRange: this.props.outputRotationRange,
      extrapolate: 'clamp',
    });

    return (
        <View {...this._panResponder.panHandlers} style={[{position:'relative'},this.props.style]}>

          {this.props.renderNoMoreCards()}

          <Animated.View
              {...this._setPointerEvents(topCard, 'cardB')}
              style={{
                position: 'absolute',
                zIndex: (topCard === 'cardB') ? 3 : 2,
                ...Platform.select({
                  android: {
                    elevation: (topCard === 'cardB') ? 3 : 2,
                  }
                }),
                transform: [
                  { rotate: (topCard === 'cardB') ? rotate: '0deg' },
                  {translateX: (topCard === 'cardB') ? drag.x: 0},
                  {translateY: (topCard === 'cardB') ? drag.y: 0},
                  { scale: (topCard === 'cardB') ? 1 : SC},
                ]
              }}>
              {cardB}
          </Animated.View>
          <Animated.View
              {...this._setPointerEvents(topCard, 'cardA')}
              style={{
                position: 'absolute',
                zIndex: (topCard === 'cardA') ? 3 : 2,
                ...Platform.select({
                  android: {
                    elevation: (topCard === 'cardA') ? 3 : 2,
                  }
                }),
                transform: [
                  { rotate: (topCard === 'cardA') ? rotate: '0deg' },
                  {translateX: (topCard === 'cardA') ? drag.x: 0},
                  {translateY: (topCard === 'cardA') ? drag.y: 0},
                  { scale: (topCard === 'cardA') ? 1 : SC},
                ]
              }}>
              {cardA}
          </Animated.View>

        </View>
    );
  }
}

CardStack.propTypes = {

  children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired,

  style: PropTypes.oneOfType([PropTypes.number, PropTypes.object, PropTypes.array]),
  secondCardZoom: PropTypes.number,
  loop: PropTypes.bool,
  renderNoMoreCards: PropTypes.func,
  onSwipeStart: PropTypes.func,
  onSwipeEnd: PropTypes.func,
  onSwiped: PropTypes.func,
  onSwipedLeft: PropTypes.func,
  onSwipedRight:PropTypes.func,
  onSwipedTop: PropTypes.func,
  onSwipedBottom: PropTypes.func,
  onSwiped: PropTypes.func,
  onSwipedAll: PropTypes.func,

  disableBottomSwipe: PropTypes.bool,
  disableLeftSwipe: PropTypes.bool,
  disableRightSwipe: PropTypes.bool,
  disableTopSwipe: PropTypes.bool,
  verticalSwipe: PropTypes.bool,
  verticalThreshold: PropTypes.number,

  horizontalSwipe: PropTypes.bool,
  horizontalThreshold: PropTypes.number,
  outputRotationRange: PropTypes.array,
  duration: PropTypes.number

}

CardStack.defaultProps = {

  style:{},
  secondCardZoom: 0.95,
  loop: false,
  renderNoMoreCards: () => { return (<Text>No More Cards</Text>)},
  onSwipeStart: () => null,
  onSwipeEnd: () => null,
  onSwiped: () => {},
  onSwipedLeft: () => {},
  onSwipedRight: () => {},
  onSwipedTop: () => {},
  onSwipedBottom: () => {},
  onSwipedAll: async () => {
    console.log('onSwipedAll')
  },

  disableBottomSwipe: false,
  disableLeftSwipe: false,
  disableRightSwipe: false,
  disableTopSwipe: false,
  verticalSwipe: true,
  verticalThreshold: height/4,
  horizontalSwipe: true,
  horizontalThreshold: width/2,
  outputRotationRange: ['-15deg','0deg','15deg'],
  duration: 200


}

Upvotes: 0

Views: 1253

Answers (1)

Liron Navon
Liron Navon

Reputation: 1097

you can take a look at this issue, regarding this problem react-native-card-stack-swiper/issues/43 - hopefully, they will fix it at some point.

The issue is that the swiper doesn't look at children when they change, so we must force it to recognize the change, in your example, you can simply use the length of the people, or the ID of the last message from firebase.

<CardStack {...props} key={this.peopleList.length} > {renderCards()} </CardStack>

Upvotes: 1

Related Questions