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.
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>
);
}