If Cards Swiped fast only then useEffect going into infinite loop

593 Views Asked by At

The issue I am facing is when I am swiping the cards really fast then my useEffect is being called infinitely which is messing up my deck of cards. Whereas my useEffect should only be called when current card Index changes but its happening everytimes .Tried a lot not able to figure out "I am noob in react/react-native". I m pasting code for my code, please help me out, any help will be appreciated

import React, {useState, useEffect} from 'react';
import {StyleSheet, Text, View} from 'react-native';
import styled from 'styled-components';
import Swiper from 'react-native-deck-swiper';
import ReactionCTAs from '../reactionCTAs/ReactionCTAs';
import MovieDetails from '../cardMovieDetails/MovieDetails';

const Recommendations = () => {
  const [currentCardIndex, setCount] = useState(0);
  const [loading, setLoading] = useState(true);
  const [cardsState, updateState] = useState({
    cards: [],
    swipedAllCards: false,
    swipeDirection: '',
    cardIndex: 0,
    pageNumber: 1,
  });

  useEffect(() => {
    console.log('Use effect being called');

    fetch(
      `https://abcde`,
      {
        method: 'GET',
        headers: {
          Accept: 'application/json',
          Authorization: 'Token ',
        },
      },
    )
      .then((response) => response.json())
      .then((json) => {
        setLoading(false);
        updateState({
          ...cardsState,
          cards: [...cardsState.cards, ...json.data],
        });
      })
      .catch((error) => {
        console.error(error);
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentCardIndex]);

  const renderCard = (card, index) => {
    return (
      <Card>
        {cardsState.cards[index] !== undefined ? (
          <CardImage
            source={{
              uri: cardsState.cards[index].media.posters[0],
            }}>
            <MovieDetails movie={cardsState.cards[index]} />
            <ReactionCTAs
              movieId={cardsState.cards[index].id}
              swipeLeft={swipeLeft}
              swipeRight={swipeRight}
              swipeTop={swipeTop}
              swipeBottom={swipeBottom}
            />
          </CardImage>
        ) : (
          <Text>Undefined</Text>
        )}
      </Card>
    );
  };

  const onSwiped = (type) => {
    setCount((prevCurrentCardIndex) => prevCurrentCardIndex + 1);
    updateState((prevState) => ({
      ...prevState,
      pageNumber: prevState.pageNumber + 1,
    }));
    if (type === 'right') {
      fetch('https://abcde', {
        method: 'POST',
        body: `{"movie":${cardsState.cards[currentCardIndex].id}}`,
        headers: {
          'Content-type': 'application/json',
          Authorization: 'Token',
        },
      });
    }
  };

  const onSwipedAllCards = () => {
    updateState({
      ...cardsState,
      swipedAllCards: true,
    });
  };

  const swipeLeft = () => {
    cardsState.swiper.swipeLeft();
  };

  const swipeRight = () => {
    cardsState.swiper.swipeRight();
  };
  const swipeTop = () => {
    cardsState.swiper.swipeTop();
  };
  const swipeBottom = () => {
    cardsState.swiper.swipeBottom();
  };

  return loading ? (
    <ProgressBar animating={true} color="red" size="large" />
  ) : (
    <Container>
      <Swiper
        ref={(swiper) => {
          cardsState.swiper = swiper;
        }}
        backgroundColor={'#20242b'}
        onSwiped={() => onSwiped('general')}
        onSwipedLeft={() => onSwiped('left')}
        onSwipedRight={() => onSwiped('right')}
        onSwipedTop={() => onSwiped('top')}
        onSwipedBottom={() => onSwiped('bottom')}
        onTapCard={swipeLeft}
        cards={cardsState.cards}
        cardIndex={cardsState.cardIndex}
        cardVerticalMargin={80}
        renderCard={renderCard}
        children={true}
        stackScale={4.5}
        onSwipedAll={onSwipedAllCards}
        stackSize={3}
        stackSeparation={-25}
        overlayLabels={{
          bottom: {
            title: 'BLEAH',
            style: {
              label: {
                backgroundColor: 'black',
                borderColor: 'black',
                color: 'white',
                borderWidth: 1,
              },
              wrapper: {
                flexDirection: 'column',
                alignItems: 'center',
                justifyContent: 'center',
              },
            },
          },
          left: {
            title: 'NOPE',
            style: {
              label: {
                backgroundColor: 'black',
                borderColor: 'black',
                color: 'white',
                borderWidth: 1,
              },
              wrapper: {
                flexDirection: 'column',
                alignItems: 'flex-end',
                justifyContent: 'flex-start',
                marginTop: 30,
                marginLeft: -30,
              },
            },
          },
          right: {
            title: 'LIKE',
            style: {
              label: {
                backgroundColor: 'black',
                borderColor: 'black',
                color: 'white',
                borderWidth: 1,
              },
              wrapper: {
                flexDirection: 'column',
                alignItems: 'flex-start',
                justifyContent: 'flex-start',
                marginTop: 30,
                marginLeft: 30,
              },
            },
          },
          top: {
            title: 'SUPER LIKE',
            style: {
              label: {
                backgroundColor: 'black',
                borderColor: 'black',
                color: 'white',
                borderWidth: 1,
              },
              wrapper: {
                flexDirection: 'column',
                alignItems: 'center',
                justifyContent: 'center',
              },
            },
          },
        }}
        animateOverlayLabelsOpacity
        animateCardOpacity
        swipeBackCard
      />
    </Container>
  );
};

const Container = styled.View`
  flex: 1;
`;

const CardImage = styled.ImageBackground`
  height: 100%;
  width: 372px;
`;

const Card = styled.View`
  flex: 1;
  border-radius: 4px;
  justify-content: center;
  background-color: #20242b;
`;

const ProgressBar = styled.ActivityIndicator`
  flex: 1;
  justify-content: center;
  align-items: center;
`;

export default Recommendations;

1

There are 1 best solutions below

1
HMR On

It is unclear what your code is supposed to do but fetching data based on user action has some pitfalls.

  1. Debounce fetching so fetch is only triggered when user is inactive for a certain amount of time.
  2. Only resolve when it was the last user action, why you should do this is explained here

So here are some helper functions you can use:

//helper to make sure promise only resolves when
//  it was the last user action
const last = (fn) => {
  const check = {};
  return (...args) => {
    const last = {};
    check.last = last;
    return Promise.resolve(fn(...args)).then((resolve) =>
      check.last === last
        ? resolve
        : Promise.reject('replaced by newer request')
    );
  };
};
const debounce = (fn, time = 300) => {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), time);
  };
};
// only resolve if it was the last user action
const lastFetch = debounce(last(fetch));
// the debounced effect (only run when inactive for 300ms)
const effect = debounce(
  (pageNumber, setLoading, updateState) => {
    //only resolve when it was the last user action
    lastFetch(`https://abcde/?page=${pageNumber}`, {
      method: 'GET',
      headers: {
        Accept: 'application/json',
        Authorization: 'Token',
      },
    })
      .then((response) => response.json())
      .then((json) => {
        setLoading(false);
        //pass callback to upateState so cardsState is not
        //  a dependency of the effect
        updateState((cardsState) => ({
          ...cardsState,
          cardIndex: 0,
          cards: [...cardsState.cards, ...json.data],
        }));
      })
      .catch((error) => {
        console.error(error);
      });
  }
);

And here is how you can run the effect:

useEffect(() => {
  //debounced and resolve last
  effect(cardsState.pageNumber, setLoading, updateState);
  //if you don't add pageNumber then pageNumber will be
  //  a stale closure and request with new pageNumber
  //  will never be made
}, [cardsState.pageNumber, cardsState.currentCardIndex]);