Search code examples
javascriptreact-nativecomponents

React Native making a download button that changes state when files are saved locally


Download Button Issue in React Native

I am trying to create a download button on a card in a React Native app. The buttons are in a card component in a FlatList. Everything renders correctly and in the right parent but on mount each button checks for files locally for the given card. When multiple buttons are created that check function runs together. Top down the FlatList is built in the following code.

The Code:

import { Button,Text ,View, SafeAreaView, StyleSheet,TouchableOpacity,FlatList } from 'react-native';
import CreateCard from "../ui/CardView";
import * as cat_1_strings  from '../constants/Cat1_Strings'
import * as cat_2_strings from '../constants/Cat2_Strings'
import React, {useState} from 'react';


const Item = ({item, origin, navigation}) => (
    <TouchableOpacity 
            onPress={() => navigation.navigate("Instructions",{source : item.id, origin: origin})} 
            style={styles.touch}
            key = {item.id}>
        <CreateCard 
            style = {styles.card}
            title = {item.title}
            body = {item.card_body_1 + "\n" +
                    item.card_body_2}
            origin = {origin}
            id = {item.id}
            firebase_loc = {item.card_image_firebase}
            filename = {item.card_image_filename}
        />
    </TouchableOpacity>
  );
  

const CardsScreen = ( {navigation, route: {params: {origin}}} ) => {
    
    switch (origin){
        case 'cat_1':
            var card_data = cat_1_strings.cat_1
            break;
        case 'cat_2':
            var card_data = cat_2_strings.cat_2
            break;
    }


    const renderItem = ({item}) => {    
        return (
          <Item
            item={item}
            origin = {origin}
            navigation = {navigation}
            key = {item.id}
          />
        );
      };


    return (
        <SafeAreaView style={styles.container}>
            <FlatList
                data={card_data}
                keyExtractor={item => item.id}
                renderItem={renderItem}
                numColumns={2}
                columnWrapperStyle={{ flexWrap: 'wrap'}}
                horizontal={false}
                contentContainerStyle={{alignItems: "stretch"}}
            />
            <Button title = {origin}/>
            
        </SafeAreaView>
    );
}
    
export default CardsScreen
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

The card function that makes the Card with the download button is here.

Card Function:

import {React, useState, useEffect}  from "react";
import { Text ,View, StyleSheet, ActivityIndicator, Image } from 'react-native';
import {Card, Button , Title ,Paragraph } from 'react-native-paper';
import { getStorage, ref, getDownloadURL } from "@react-native-firebase/storage";
import DownloadButton from "./DownloadButton";
import * as FileSystem from 'expo-file-system';

const CreateCard = props => {

  const storage = getStorage();

  const def_img = require('../assets/favicon.png');
  const def_imgURI = Image.resolveAssetSource(def_img).uri;
  const [localImageUri, setLocalImageUri] = useState(def_imgURI);

  useEffect(() => {
    //
    // There is logic here to setLocalImageUri that works
    //
    //
  }, []);

  return(
        
    <Card style={props.style}
      key = {props.id} >
      <Card.Content>
          <Title>{props.title}</Title>
      </Card.Content>
      <Card.Cover source = {{uri : localImageUri}} />
      <Card.Content>
        <Paragraph>{props.body}</Paragraph>
      </Card.Content>
      <Card.Actions>
        <DownloadButton
          id = {props.id}
          origin = {props.origin}
          key={props.id}
          >
        </DownloadButton>
      </Card.Actions>
    </Card>
  )
}
export default CreateCard;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

And finally the download button logic.

Download Button Code:

import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, Alert, StyleSheet } from 'react-native';
import * as FileSystem from 'expo-file-system';
import * as cat_1_strings  from '../constants/Cat1_Strings'
import * as cat_2_strings from '../constants/Cat2Strings'
import ImageDownload  from "../services/imageDownloader"
import ImageDeleter  from "../services/imageDeleter"

const DownloadButton = props => {
  
    const [filesExist, setFilesExist] = useState(false);

    source = props.id
    origin = props.origin

    const [buttonTitle, setButtonTitle] = useState('Download');

    useEffect(() => {
      checkFilesExist()
    }, []);


    const checkFilesExist = async () => {

        switch (origin){
            case 'cat_1':
                full_data_array = card_games_strings.cardgames
                data = full_data_array.find(item => item.id === source)
                break;
            case 'cat_2':
                full_data_array = knots_strings.knots
                data = full_data_array.find(item => item.id === source)
                break;
        }

        inst_items = data.inst_items

        num_images = 0
        num_downloaded = 0

        for(let i=0;i<inst_items.length;i++){

            
            if(inst_items[i].style === "image"){
                
                num_images++
                console.log("Checking Source: " + source)
                console.log("Num images: " + num_images)

                console.log(inst_items[i].filename)

                console.log("")

                const localUri = FileSystem.documentDirectory + inst_items[i].filename;
                const fileInfo = await FileSystem.getInfoAsync(localUri);

                if (fileInfo.exists) {
                    num_downloaded++
                }
            }
        }

        console.log("num images: " + num_images)
        console.log("num downloaded: " + num_downloaded)

        if (num_images === num_downloaded){
            const filesExist = true;
            setFilesExist(true)

            console.log("all exist")
        } else {
            const filesExist = false;
            console.log("not all exist")
            setFilesExist(false)
        }
    };
  
    // Function to handle the download or delete action
    const handleAction = () => {

        console.log("in handle action")
        if (filesExist) {
            ImageDeleter(origin,props.id)

            Alert.alert('Files Deleted', 'Files have been deleted successfully.');
            setFilesExist(false); // Update state to indicate files have been deleted


        } else {

            ImageDownload(props.id)
            Alert.alert('Files Downloaded', 'Files have been downloaded successfully.');
            setFilesExist(true); // Update state to indicate files have been downloaded

        }
    };
  
    return (
      
      <View style={styles.container} key = {props.id}>
        <TouchableOpacity onPress={handleAction} style={styles.button} >
          <Text style={styles.buttonText}>{filesExist ? 'Delete' : 'Download'}</Text>
        </TouchableOpacity>
      </View>
    );
  };
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

In the console the for-loop is mixing data together. The num_images and num_downloaded are added as both buttons are going through the loop. Additionally some of the items in the inst_items array are looped through twice, I am guessing because it is also changing between them. I am not sure why they are not running independently through the loop. I have no warning about unique keys and they should have them as everything renders correctly. I just cant get the logic to for checking if the files to exist to run on its own.


Solution

  • For anyone that finds this online. I found a solution, not sure it is the best but it works for me. I changed my button into a class so I could use the this keyword. So this is my now working code (needs some cleanup and styling but basics are there) and apologies for the struggle I am having to format this on stack overflow:

    import React  from "react";
    import { View, Text, TouchableOpacity, Alert } from 'react-native';
    import * as FileSystem from 'expo-file-system';
    import ImageDownload  from "../services/imageDownloader"
    import ImageDeleter  from "../services/imageDeleter"
    
    
    
    export default class DownloadButton2  extends React.Component {
      constructor(props) {
      super();
    
      this.source = props.id;
      this.origin = props.origin;
      this.inst_items = props.inst_items;
      this.num_images = 0;
      this.num_down = 0;
      this.id = props.id
      this.state = {
         filesExist : false
      }
    
    }
    
    async componentDidMount() {
        for(item of this.inst_items){
          if(item.style === "image"){
            this.num_images++
            
           const localUri = FileSystem.documentDirectory + item.filename;
           const fileInfo = await FileSystem.getInfoAsync(localUri);
    
           if (fileInfo.exists) {
                this.num_down++
            }
          }
        }
    
    
    
        if (this.num_images === this.num_down){      
            this.setState({filesExist: true})
    
            console.log("all exist")
        } else {
            
            console.log("not all exist")
            this.setState({filesExist: false})
        }
    }
    
    
    _handleOnButtonPress = () => {
        if (this.state.filesExist) {
            ImageDeleter(this.origin,this.id)
    
            Alert.alert('Files Deleted', 'Files have been deleted successfully.');
            this.setState({filesExist: false}); // Update state to indicate files have been deleted
    
    
       } else {
    
            ImageDownload(this.id)
            // TODO: handle this better and make it async (maybe something up top on the tool bar)
            Alert.alert('Files Downloaded', 'Files have been downloaded successfully.');
            this.setState({filesExist: true}); // Update state to indicate files have been downloaded
        }
    }
    
    render(){
      return (
        <View  key = {this.id}>
          <TouchableOpacity onPress={this._handleOnButtonPress} >
            <Text>{this.state.filesExist ? 'Delete' : 'Download'}</Text>
          </TouchableOpacity>
        </View>
      );
    

    }