API call function in useEffect creates infinite loop in React Native - react-native

I have a Component where i'm maping an Array to render data. But before that, i need to call API taking id from each objects of the array to modify the array. Now, i am calling the API's in a function and calling the function in useEffect() . But when i do that, it continues to an infinite loop. Here's how my component looks like:
const DemoComponent = (props) => {
const [renderArr, setRenderArr] = useState([]);
useEffect(() => {
getStatus();
},[renderArr])
const getStatus= async() =>{
var arr = [
{id: 1,name: Leather},
{id: 2,name: Shoe},
{id: 3,name: Belt},
]
var firstStatus = await API(arr[0].id , props.token)
var secondStatus = await API(arr[1].id , props.token)
var thirdStatus = await API(arr[2].id , props.token)
var statusObj = [
{ status: firstStatus.status },
{ status: secondStatus.status },
{ status: thirdStatus.status },
]
var mergedArray = newArr.map((e, i) => ({ ...e, ...statusObj[i] }));
setRenderArr(mergedArray);
}
}
return (
<View style={styles.container}>
{mergedArray.map((item, index) => {
return (
<TouchableOpacity>
<Text style={{ color: '#FFF' }}>{item.status}</Text>
</TouchableOpacity>
);
})}
</View>
);
};
Now, how can i stop this infinite loop. But in the meantime, i want to rerender when renderArr changes props.

Because you trigger the renderArr. You should do one call only when the component is mounted such as below snippet.
useEffect(() => {
getStatus();
}, []);

There are a few items I would like to point out here,
You have side effect registered for renderArr, which calls a function that updates renderArr. Any change in renderArr will invoke the side effect and this loop goes on forever.
Inside the getStatus function, you are updating your renderArr state after your application logic. But the render part is referring to some other variable mergedArray. You will have to change it to renderArr state.
const DemoComponent = (props) => {
const [renderArr, setRenderArr] = useState([]);
useEffect(() => {
getStatus();
},[]); // removed dependency to call it only once, (on mount)
const getStatus= async() =>{
....
setRenderArr(mergedArray); // state is updated with the new processed value
}
return (
<View style={styles.container}>
{renderArr.map((item, index) => { // changed to the state variable
return (
<TouchableOpacity>
<Text style={{ color: '#FFF' }}>{item.status}</Text>
</TouchableOpacity>
);
})}
</View>
);
};

Related

how to set UI component to state in background in react-native?

I am new in react-native, in my application I am generating QRCode by one library and it working but in android it is taking time to show on UI, when I set that component to hook to show on UI then it stuck for while and every thing getting non-responsive. After some time it shows and everything work well.
So how can put that setWalletQR in background so that I can show loader until it show to UI?
Here is my code where I am generating the QR in InteractionManager to show
const PozReceive = ({ onClose }: ReceiveProps) => {
const [walletQR, setWalletQR] = useState<ConentQR>(null);
const generateWalletQrCode = () => {
const interactionPromise = InteractionManager.runAfterInteractions(() => {
const qrCode = ConentQR(user?.walletAddress || '', walletImg, 50);
setWalletQR(qrCode);
});
return () => interactionPromise.cancel();
};
useEffect(() => {
if (!pouchQR) {
generatePouchQrCode();
}
}, []);
return (
<Modal
coverScreen={true}
isVisible={true}
onBackdropPress={onClose}
onBackButtonPress={onClose}
backdropColor={Colors.DARK_PURPLE}
backdropOpacity={0.7}
style={styles.modal}>
<>
<BlurView
style={styles.blurView}
blurType="dark"
blurAmount={20}
reducedTransparencyFallbackColor="white"
/>
<VStack style={[styles.modalContainer]}>
{!walletQR ? (
<Image style={styles.qrLoader} source={loaderGif} />
) : (
walletQR
)}
</VStack>
</>
</Modal>
);
};
and here is QR code generator code :-
const ContentQR = (
content: string,
logo: Image.propTypes.source,
logoSize: number,
backgroundColor: string = 'transparent',
) => {
return (
<QRCode
color={Colors.DARK_PURPLE}
content={content}
codeStyle={'dot'}
outerEyeStyle={'diamond'}
logo={logo}
logoSize={logoSize}
backgroundColor={backgroundColor}
/>
);
};
Someone please help me I getting stuck here for while.
You can introduce a variable isLoading and render the loader based on this variable instead of qr value.
const PozReceive = ({ onClose }: ReceiveProps) => {
const [walletQR, setWalletQR] = useState<ConentQR>(null);
const [isLoading, setIsLoading] = useState<Boolean>(false);
const generateWalletQrCode = () => {
setIsLoading(true)
const interactionPromise = InteractionManager.runAfterInteractions(() => {
const qrCode = ConentQR(user?.walletAddress || '', walletImg, 50);
setWalletQR(qrCode);
setIsLoading(false)
});
return () => interactionPromise.cancel();
};
....
<VStack style={[styles.modalContainer]}>
{isLoading && <Image style={styles.qrLoader} source={loaderGif} />}
{!isLoaing && walletQR && walletQR}
</VStack>

onEndReached not called when new data on same page

So when I first load my component it fetches data from the server. The data is displayed and takes up less space than the screen. Shouldn't onEndReached be called in this situation? Or is my assumption incorrect because the data wasn't scrolled to? From my testing, onEndReached is not called on Android & IOS but I want it to.
/**
* https://reactnavigation.org/docs/4.x/typescript
*/
type Props = {
navigation: NavigationDrawerProp<{ userId: string, routeName: string }>;
}
let idCounter = 0;
export const keyExtractor = (item: any) => {
if (!item.uniqueId) {
item.uniqueId = idCounter;
idCounter++;
}
return item.uniqueId.toString();
}
const MasterScreen = (props: Props) => {
const [data, setData] = useState([]);
const loadDataFirstTime = () => {
setTimeout(() => {
let newData = [];
for (let i = 0; i < 2; i++) {
newData.push({})
}
setData(newData)
}, 4000)
}
useEffect(() => {
loadDataFirstTime();
}, []);
const renderItem = () => {
return <Text>Placeholder</Text>
}
const onEndReached = () => {
console.log("End reached with data length", data.length)
Alert.alert("Data length: " + data.length)
}
return (
<SafeAreaView style={{ flex: 1, height: height }}>
<FlatList data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
/>
</SafeAreaView>
);
}
Using react native 0.63.4
EDIT:
Sample github repo
Relevant code inside src/screens/MasterScreen.tsx
onEndReached is not called on load but you will see the FlatList rendered
To test:
yarn
yarn android

AsyncStorage use boolean from Promise

hi i'm new on react native and i have a issue with asyncStorage. I want to store the cache state in my pdf screen. The cache is a parameter of the source and handle only boolean. I made an onPress which change a state and store it in my localstorage, it works and when i console.log my getItem it shows true or false too it works too. But here is my problem. Now i want to just use the true or the false from this getItem because the parameter cache can handle boolean only. The best i could get on my search was Promise Boolean for my function. So if you could help me it'll be incredible because i really don't know. Thank you a lot and sorry for my English.
Here's my code //
export class Liste extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
navigation : props.navigation,
route: props.route,
selectedIndex : this.selectedIndex,
page : this.page,
numberOfPages : this.numberOfPages,
filePath : [],
cache : false,
};
}
saveCache() {
AsyncStorage.setItem('cache', JSON.stringify(this.state.cache));
console.log(`store ${this.state.cache}`);
}
async getCache () {
const ta = await AsyncStorage.getItem('cache', (value) => {
JSON.parse(value)
})
console.log(ta)
}
navigateBack = () => {
this.state.navigation.goBack();
};
BackAction = () => (
<TopNavigationAction icon={BackIcon} onPress={this.navigateBack}/>
);
render() {
const {files} = this.state.route.params;
const cache = this.state.cache;
const bool = this.getCache();
return (
<>
<TopNavigation style={{ borderWidth: 1 }} title='Mes Articles' alignment='center' accessoryLeft={this.BackAction} />
<ViewPager
selectedIndex={this.state.selectedIndex}
onSelect={ index => this.setState({ selectedIndex: index })}>
{files.map((file, i) =>
<Layout style={styles.tab} level='2'>
<Text>{file.filename}</Text>
<Text>Article: {i + 1} / {files.length} page: {this.state.page} / {this.state.numberOfPages}</Text>
<View>
<TopNavigationAction icon = {emailIcon} onPress={() => Share.open({ title: 'Pdf file', message: `bonjour voici l'article pdf ${file.filename}`, url: `file:///${this.state.filePath[i]}`, subject: `Article Pdf ${file.filename}` })} status='Partager'>
Partager
</TopNavigationAction>
<TopNavigationAction icon = {pin} onPress ={() => this.saveCache(cache === true ? this.setState({cache : false}) : this.setState({cache : true}))} status='Partager'>
Partager
</TopNavigationAction>
<TopNavigationAction icon = {pin} onPress ={() => console.log(this.getCache())} status='Partager'>
Partager
</TopNavigationAction>
</View>
<Pdf
source={{ uri: `http://10.1.0.248/${file.path}/${file.filename}`, cache : bool}}
style={styles.pdf}
enablePaging={true}
onLoadComplete={(numberOfPages, filePath) => {
this.state.filePath.push(filePath);
this.setState({ numberOfPages: numberOfPages });
}}
onPageChanged={(page, numberOfPages) => {
this.setState({ page: page });
}}
/>
</Layout>
)}
</ViewPager>
</>
);
}
}
You can use it like this.
await AsyncStorage.getItem('cache'); returns a JSON stringified value which you could parse and use.
async getCache () {
const ta = await AsyncStorage.getItem('cache');
console.log(JSON.parse(ta))
}
Use it likewise
let ta = await AsyncStorage.getItem('cache');
ta = JSON.parse(ta);

Unhandled Promise Rejection : Existing value for key "Favorites" must be of type null or array, revived string

my problem is quite simple but I'm new to react native dev. I'd like to save multiple elements with AsyncStorage (I'm using react-native-simple-store
a library that works like a wrapper but it's same logic) I want display all items for a key in a list , my code look like this:
constructor(props) {
super(props)
this.state = {
UserInput: "",
}
}
SaveValue = () => {
store.push('Favorites', this.state.UserInput)
Keyboard.dismiss()
};
FetchValue = () => {
store.get('Favorites').then((value) => {
this.setState({
favs: value
});
}).done();
};
Same thing with AsynStorage, it just update the item which is not my goal, I'd like to add a new one
SaveValue = () => {
AsyncStorage.setItem("Favorites", this.state.UserInput);
Keyboard.dismiss()
};
FetchValue = () => {
AsyncStorage.getItem("Favorites").then((value) => {
this.setState({
favs: value
});
}).done();
};
This part is my view where I try to display data, you can also see that I use a text input and two buttons one to save and the other to display an array of items stored
render() {
return (
<View>
<TextInput
onChangeText={(UserInput) => this.setState({UserInput})}
placeholder= "Type something"
value={this.state.UserInput} />
<TouchableOpacity
onPress={this.SaveValue}>
<Text>Save</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.FetchValue}>
<Text>Fetch</Text>
</TouchableOpacity>
<Text>{this.state.favs}</Text>
</View>
);
}
At this point I can see only one item, I tried to figure it out and saw that I have to use another method called push but when I changed save by push it throw me an error
Unhandled Promise Rejection : Existing value for key "Favorites" must be of type null or array, revived string.
Thanks!
it will work :)
renderFavorites = () => {
AsyncStorage.getItem("Favorites").then((favs) => {
favs.map((fav) => {
return (<Text> {fav} </Text>);
});
});
}
render() {
return (
<View>
<TextInput
onChangeText={(UserInput) => this.setState({UserInput})}
placeholder= "Type something"
value={this.state.UserInput} />
<TouchableOpacity
onPress={this.SaveValue}>
<Text>Save</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.FetchValue}>
<Text>Fetch</Text>
</TouchableOpacity>
{this.renderFavorites()}
</View>
);
}
Solution using JSON:
SaveValue = () => {
const newFavs = [...this.state.favs, this.state.UserInput];
this.setState({ favs: newFavs, UserInput: '' }, () => {
AsyncStorage.setItem("Favorites", JSON.stringify(this.state.favs));
Keyboard.dismiss()
});
};
FetchValue = () => {
AsyncStorage.getItem("Favorites").then((value) => {
this.setState({
favs: JSON.parse(value)
});
}).done();
};

How to Render Realm ListView with Sections Header

I'm using react native with realm db. The realm schema is as follows:
static schema = {
name: 'TodoItem',
primaryKey: 'id',
properties: {
id: {type: 'string'},
value: {type: 'string'},
Category: {type: 'string'},
completed: {type: 'bool', default: false},
createdTimestamp: {type: 'date'}
}
}
export const todoItemDS = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2, sectionHeaderHasChanged: (s1, s2) => s1 !== s2})
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(todoItemsResults),
}
The ListView tag is as follows:
<ListView
dataSource={dataSource}
renderRow={this.renderRow.bind(this)}
renderSectionHeader={this.renderSectionHeader.bind(this)}
/>
and renderSectionHeader:
renderSectionHeader(sectionData, category) {
return (
<Text>{category}</Text>
)
}
renderRow(item){
const {dataSource, deleteTodoItem} = this.props
return (
<View style={{ justifyContent: 'space-between',flexDirection: 'row'}}>
<CheckBox onPress={(e) => this.completed(item.id,item.value,e.target.checked)} style={{marginTop: 15 }}checked={item.completed} />
<Text onPress={(e) => this.goToPageTwo(item.id)} style={{ alignSelf: 'center',flex:10}} >{item.value}
</Text>
<Button iconLeft large transparent primary style={{ height: 30 , flex:1 }} onPress={() => deleteTodoItem(item)}>
<Icon name="trash-o" style={{ color: 'red' }} />
</Button>
</View>)
}
I fill todoItems datasource from this function:
export const getTodoItems = () => {
const todoItems = TodoItem.get().sorted('createdTimestamp', true);
return todoItems
}
However, the rows and sections are rendered with empty sections text and empty rows text as shown in the image.
What is missing in this code and how can I render sections and rows correctly?
I added a listener to realm code that fills the data source as follows:
export const getTodoItems = () => {
console.log('create db:', Realm.path)
const itemData = {}
const todoItems = TodoItem.get().sorted('createdTimestamp', true).filtered('completed=false');
todoItems.addListener((items, changes) => {
// Update UI in response to inserted objects
changes.insertions.forEach((index) => {
if(itemData[items[index].Category]) {
itemData[items[index].Category].push(items[index])
} else
itemData[items[index].Category] = []//;
});
// Update UI in response to modified objects
changes.modifications.forEach((index) => {
});
// Update UI in response to deleted objects
changes.deletions.forEach((index) => {
// Deleted objects cannot be accessed directly
// Support for accessing deleted objects coming soon...
});
});;
todoItems.forEach((item) => {
if(itemData[item.Category]) {
itemData[item.Category].push(item)
} else
itemData[item.Category] = []//;
})
return itemData //todoItems
}
However, I can't see added items. The added item only shows up after adding another item. Any ideas?
The rendered SectionHeader is showing integer is because you didn't construct the dataSource in the right format, see the documentation here: link.
You need to construct something like:
const todoItemsData = {
categoryOne: [itemOne, itemTwo],
categoryTwo: [itemThree, itemFour]
}
But right now what you have is just an array of objects [realmItemOne, realmItemTwo], and if you just pass in an array of objects to construct the dataSource that gonna consumed by the cloneWithRowsAndSections, the category param in renderSectionHeader will becomes integer index accroding to here, Object.keys(arrayOfObjects) will return [0, 1, 2, ...]
So you want to map your todoItemsResults and construct the something like this
const itemData = {}
todoItemsResults.forEach((item) => {
if(itemData[item.Category] {
itemData[item.Category].push(item)
} else
itemData[item.Category] = [];
}
});
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(itemData),
}
For the rendered row not showing any data issue, I think is due to the same problem, the item param you are calling within renderRow should be a data attribute for one of you todoItem object. You can try replace {item.value} within {item} to see what item is actually is in your setting. I believe this will be solved if you can construct the right data to feed your dataSource.
For your follow up comments regarding listerning to Realm update:
You can do something like this
class DummyPage extends Component {
constructor(props) {
super(props)
this.realmUpdated = this.realmUpdated.bind(this);
realm.objects('todoItem').addListener('change', this.realmUpdated);
}
componentWillUnmount() {
realm.objects('todoItem').removeListener('change', this.realmUpdated);
}
realmUpdated() {
this.forceUpdate();
}
render() {
//build your data source in here and render the sectionList
}
}