Search code examples
javascriptreact-nativereact-native-deck-swiper

How to re render cards in a swiper using react-native-deck-swiper


I'm facing an issue with updating the content of cards in a swiper using react-native-deck-swiper. I have a button external to the swiper, and when pressing it I want to update the content of the current card and trigger a re-render to reflect the changes.

I've managed to update the state of the item correctly, but the card doesn't re-render immediately. It only renders again when I start swiping the item.

The documentation suggests a possible fix, stating, "A possible fix for the situation is setting the cardIndex on the parent component whenever deck re-renders are needed." However, my attempt to implement this hasn't been successful.

I have noticed that the change is render when the overlay condition are required (even if no overlay is set).

Below is a reproducible example of the problem :

import React, { useRef, useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import Swiper from 'react-native-deck-swiper';

const cards = [
    { color: 'red', updated: false },
    { color: 'blue', updated: false },
    { color: 'green', updated: false },
    { color: 'yellow', updated: false },
    { color: 'purple', updated: false },
];

export default function App() {
    const [cardIndex, setCardIndex] = useState(0);
    const swiperRef = useRef(null);

    const updateCard = () => {
        cards[cardIndex].updated = !cards[cardIndex].updated;
        // Force re-render??
        setCardIndex(cardIndex); 
    };

    const onSwiped = () => {
        setCardIndex((cardIndex + 1) % cards.length); 
    };

    const renderCard = (card) => (
        <View style={[styles.card, { backgroundColor: card.color }]}>
            {card.updated && <Text style={styles.updatedText}>UPDATED</Text>}
        </View>
    );

    return (
        <View style={styles.container}>
            <Swiper
                ref={swiperRef}
                cards={cards}
                renderCard={renderCard}
                onSwiped={onSwiped}
                onSwipedLeft={onSwiped}
                onSwipedRight={onSwiped}
                cardIndex={cardIndex}
                infinite
            />
            <TouchableOpacity style={styles.button} onPress={updateCard}>
                <Text style={styles.buttonText}>Update card</Text>
            </TouchableOpacity>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#f7f7f7',
        alignItems: 'center',
        justifyContent: 'center',
    },
    card: {
        width: '80%',
        height: '80%',
        justifyContent: 'center',
        alignItems: 'center',
        alignSelf: 'center',
      },
    updatedText: {
        position: 'absolute',
        fontSize: 24,
        fontWeight: 'bold',
        color: 'black',
    },
    button: {
        position: 'absolute',
        bottom: 20,
        padding: 10,
        backgroundColor: 'blue',
        borderRadius: 5,
    },
    buttonText: {
        color: 'white',
        fontSize: 16,
    },
});

I'd appreciate any insights on how to solve this issue and get the card to re-render immediately upon pressing the button. Thank you!


Solution

  • At first, I thought the issue was that you werent storing the cards in a state variable, but after doing so, I found that the cards still wouldnt update, which makes me believe that Swiper component caches card on initialization and ignore all updates to it. This where the key prop comes to play. A component will be reinitialized when its key changes, so if you store the swiperKey in state and the update it, cards will be reinitialized (demo):

    import React, { useRef, useState } from 'react';
    import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
    import Swiper from 'react-native-deck-swiper';
    
    
    
    const getRandomInt = (min=0,max=1)=>{
      const range = max - min
      return Math.floor(Math.random()*range)+min
    }
    
    const initialCardState = [
      { color: 'red', updated: false, },
      { color: 'blue', updated: false, },
      { color: 'green', updated: false, },
      { color: 'yellow', updated: false, },
      { color: 'purple', updated: false, },
    ];
    // since this function doesnt depend on state move it outside of
    // App; otherwise will be re-created on every render
    const RenderCard = (card) => (
      <View style={[styles.card, { backgroundColor: card.color }]}>
        {card.updated && <Text style={styles.updatedText}>UPDATED</Text>}
      </View>
    );
    export default function App() {
      const [cardIndex, setCardIndex] = useState(0);
      // since cards values can change should be stored in state
      const [cards, setCards] = useState(initialCardState);
      const swiperRef = useRef(null);
      const [swiperKey, setSwiperKey] = useState("swiper-key")
      const updateCard = () => {
        setCards((prev) => {
          const newCards = [...prev];
          newCards[cardIndex].updated = !newCards[cardIndex].updated;
          return newCards;
        });
        // since setting the state isnt enough to get
        // the swiper component to re-render
        setSwiperKey(prev=>prev+getRandomInt(0,10))
    
      };
    
      const onSwiped = () => {
        // use the callback version of state setters
        // for better reliability
        setCardIndex((prev) => (prev + 1) % cards.length);
      };
    
      return (
        <View style={styles.container}>
          <Swiper
            key={swiperKey}
            ref={swiperRef}
            cards={cards}
            renderCard={RenderCard}
            onSwipedLeft={onSwiped}
            onSwipedRight={onSwiped}
            cardIndex={cardIndex}
            infinite
          />
          <TouchableOpacity style={styles.button} onPress={updateCard}>
            <Text style={styles.buttonText}>Update card</Text>
          </TouchableOpacity>
        </View>
      );
    }
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: '#f7f7f7',
        alignItems: 'center',
        justifyContent: 'center',
      },
      card: {
        width: '80%',
        height: '80%',
        justifyContent: 'center',
        alignItems: 'center',
        alignSelf: 'center',
      },
      updatedText: {
        position: 'absolute',
        fontSize: 24,
        fontWeight: 'bold',
        color: 'black',
      },
      button: {
        position: 'absolute',
        bottom: 20,
        padding: 10,
        backgroundColor: 'blue',
        borderRadius: 5,
      },
      buttonText: {
        color: 'white',
        fontSize: 16,
      },
    });