webraptor / react-native-deck-swiper

tinder like react-native deck swiper
ISC License
138 stars 84 forks source link

Cards not updating when state changes after all cards were swiped [Possible Solution] #94

Open nofacez opened 1 year ago

nofacez commented 1 year ago

❗️ First of all, if you are updating the cards list dynamically (e.g. adding new cards when there are 5 cards left in the current stack) and you are still facing the issue when the list doesn't update, please, make sure you have the latest version of the package. This issue should be fixed there.

Now, let's talk about our patient. Swiper doesn't update state after all cards were swiped because it considers the stack is finished and it stops adding new cards to it. Here the part of the code responsible for this:

  renderStack = () => {
    const { firstCardIndex, swipedAllCards } = this.state
    const { cards } = this.props
    const renderedCards = []
    let { stackSize, infinite, showSecondCard } = this.props
    let index = firstCardIndex
    let firstCard = true
    let cardPosition = 0

    while (stackSize-- > 0 && (firstCard || showSecondCard) && !swipedAllCards) {
      const key = this.getCardKey(cards[index], index)
      this.pushCardToStack(renderedCards, index, cardPosition, key, firstCard)

      firstCard = false

      if (index === cards.length - 1) {
        if (!infinite) break
        index = 0
      } else {
        index++
      }
      cardPosition++
    }
    return renderedCards
  }

swipedAllCards is a state variable, that is being set here

  incrementCardIndex = onSwiped => {
    const { firstCardIndex } = this.state
    const { infinite } = this.props
    let newCardIndex = firstCardIndex + 1
    let swipedAllCards = false

    this.onSwipedCallbacks(onSwiped)

    const allSwipedCheck = () => newCardIndex === this.props.cards.length

    if (allSwipedCheck()) {
      if (!infinite) {
        this.props.onSwipedAll()
        // onSwipeAll may have added cards
        if (allSwipedCheck()) {
          swipedAllCards = true // look here
        }
      } else {
        newCardIndex = 0;
      }
    }

    this.setCardIndex(newCardIndex, swipedAllCards)
  }

So once it is set to true it won't ever change to false because that stack just stops rendering.

How did I manage to fix it?

❗️DISCLAIMER: I don't think this is the best solution. This is why I've opened this issue, so we could find a more universal solution.

I've moved swipedAllCards property from state to props because I think we can decide ourselves when the list is finished or not. I've also added a isLoading prop to allow managing loading state of the swiper. To prevent Swiper from stopping rendering cards inside while case I've added CardListLoadingComponent and CardListEmptyComponent that are shown during the loading and empty state. Here is my renderStack function:

  renderStack = () => {
    const { firstCardIndex } = this.state;
    const { swipedAllCards } = this.props;
    const renderedCards = [];
    let {
      cards,
      stackSize,
      infinite,
      showSecondCard,
      loading,
      CardListLoadingComponent,
      CardListEmptyComponent,
    } = this.props;
    let index = firstCardIndex;
    let firstCard = true;
    let cardPosition = 0;

    if (swipedAllCards && loading) { // check for loading
      return CardListLoadingComponent;
    }

    if (swipedAllCards && !loading) { // check for empty
      return CardListEmptyComponent;
    }

    while (stackSize-- > 0 && (firstCard || showSecondCard)) {
      const key = this.getCardKey(cards[index], index);
      this.pushCardToStack(renderedCards, index, cardPosition, key, firstCard);

      firstCard = false;

      if (index === cards.length - 1) {
        if (!infinite) break;
        index = 0;
      } else {
        index++;
      }
      cardPosition++;
    }
    return renderedCards;
  };

And here is my incrementCardIndex function:

  incrementCardIndex = (onSwiped) => {
    const { firstCardIndex } = this.state;
    const { infinite, swipedAllCards } = this.props;
    let newCardIndex = firstCardIndex + 1;

    this.onSwipedCallbacks(onSwiped);

    const allSwipedCheck = () => newCardIndex === this.props.cards.length;

    if (allSwipedCheck()) {
      if (!infinite) {
        this.props.onSwipedAll();
      } else {
        newCardIndex = firstCardIndex;
      }
    }

    this.setCardIndex(newCardIndex, swipedAllCards);
  };

After this refactoring I had a working Deck Swiper so I stopped looking for a more reliable and universal solution. We have a heavy backend request for the new stack of cards, so while the request is pending the loading props is set to true and loading component is shown. When backend stops sending new cards the swipedAllCards is set to true and empty component is shown.

mkhoussid commented 9 months ago

Why not just set an integer as a key property on the Swiper component and just increment it whenever onSwiped fires?

astevensLogi commented 8 months ago

Why not just set an integer as a key property on the Swiper component and just increment it whenever onSwiped fires?

This was the much easier fix for me!