React Native: Variable state not updated on first click - react-native

I am new in react-native and i am working on an app.
The below code is a simple react-native app which has a custom component with custom events.
But the problem is the variable state is not updated on the first click on the component. But when i click on the second item, The state of the variable is updated.
Please find the code and screenshot below.
App.js
import React, {useState} from 'react';
import { Text, SafeAreaView, ToastAndroid } from 'react-native';
import Dropdown from './components/dropdown';
const app = () => {
const [ itemData, setItemData ] = useState('');
return (
<SafeAreaView style={{ margin: 50 }}>
<Dropdown
onPressItems={(item) => {
ToastAndroid.show('item: ' + item, ToastAndroid.LONG)
setItemData(item)
ToastAndroid.show('setItem: ' + itemData, ToastAndroid.LONG)
}}/>
</SafeAreaView>
);
}
export default app;
Dropdown.js
import React, { useState } from 'react';
import { TouchableOpacity, Text } from 'react-native';
const Dropdown = (props) => {
return (
<TouchableOpacity onPress={() => { props.onPressItems('this is sample data') }}>
<Text>Sample Text</Text>
</TouchableOpacity>
);
}
export default Dropdown;
Screenshot
Code: https://snack.expo.dev/#likithsai/custom-component
Please help me on this issue. Thanks.

useState() hook changes the state asynchronously. so you can't make sure that the state will be changed immediately after calling setItemData() function.
Try useEffect to run a side effect whenever the state changes.
useEffect(() => {
ToastAndroid.show("setItem: " + itemData, ToastAndroid.LONG);
}, [itemData]);
However, this code will show the toast on the component mount. to prevent it try something like this:
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
ToastAndroid.show("setItem: " + itemData, ToastAndroid.LONG);
}
}, [itemData]);

Your code is working just as expected. One of the hooks that are useful to watch for state updates is useEffect. You can add this to your code and see it's working properly:
const app = () => {
const [ itemData, setItemData ] = useState('');
React.useEffect(() => {
console.log('updated itemData:', itemData)
}, [itemData])
return (
<SafeAreaView style={{ margin: 50 }}>
<Dropdown
onPressItems={(item) => {
ToastAndroid.show('item: ' + item, ToastAndroid.LONG)
setItemData(item)
ToastAndroid.show('setItem: ' + itemData, ToastAndroid.LONG)
}}/>
</SafeAreaView>
);
}
You need to take into consideration that useState updates are asynchronous, which means the change won't be reflected immediately.

Related

Warning: React has detected a change in the order of Hooks

I have run into this error in my code, and don't really know how to solve it, can anyone help me?
I get the following error message:
ERROR Warning: React has detected a change in the order of Hooks called by ScreenA. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
import React, { useCallback, useEffect, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { useNavigation } from '#react-navigation/native';
import { DancingScript_400Regular } from "#expo-google-fonts/dancing-script";
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Keep the splash screen visible while we fetch resources
await SplashScreen.preventAutoHideAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({ DancingScript_400Regular });
// Artificially delay for two seconds to simulate a slow loading
// experience. Please remove this if you copy and paste the code!
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
const navigation = useNavigation();
const onPressHandler = () => {
// navigation.navigate('Screen_B', { itemName: 'Item from Screen A', itemID: 12 });
}
return (
<View style={styles.body} onLayout={onLayoutRootView}>
<Text style={styles.text}>
Screen A
</Text>
<Pressable
onPress={onPressHandler}
style={({ pressed }) => ({ backgroundColor: pressed ? '#ddd' : '#0f0' })}
>
<Text style={styles.text}>
Go To Screen B
</Text>
</Pressable>
<Text style={styles.text}>{route.params?.Message}</Text>
</View>
)
}
const styles = StyleSheet.create({
body: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 40,
margin: 10,
fontFamily: 'DancingScript_400Regular'
}
})
I have read the rules of hooks: https://reactjs.org/docs/hooks-rules.html
The output is correct, but i want to fix this error before i add more additions to the app
You need to move useNavigation use before early returns.
Instead, always use Hooks at the top level of your React function, before any early returns.
The key is you need to call all the hooks in the exact same order on every component lifecycle update, which means you can't use hooks with conditional operators or loop statements such as:
if (customValue) useHook();
// or
for (let i = 0; i< customValue; i++) useHook();
// or
if (customValue) return;
useHook();
So moving const navigation = useNavigation(); before if (!appIsReady) {return null;}, should solve your problem:
export default function ScreenA({ route }) {
const [appIsReady, setAppIsReady] = useState(false);
const navigation = useNavigation();
// ...
}

React Native GraphQL nested data array returning error

I have tried everything I can think of to solve this and am still stumped. I am using AWS AppSync GraphQL to store a dataset that I would like to call into a SectionList.
For the SectionList I am using a hardcoded id to call the data set through a GraphQL query. The SectionList displays correctly when I am using dummy data. It also displays the 1-to-1 relationships in the API correctly.
I already configured amplify to increase the statement depth and I can see the data in the Object.
Code for the SectionList
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Text, Image, ImageBackground, ScrollView, TouchableOpacity, SectionList, SafeAreaView } from 'react-native';
import Feather from 'react-native-vector-icons/Feather';
import AntDesign from 'react-native-vector-icons/AntDesign';
import { API, graphqlOperation } from 'aws-amplify';
import { getGame, listGameSections, listGames } from '../graphql/queries';
const Item = ({ title }) => (
<View>
<Text>
{title}
</Text>
</View>
);
const GameScreen = ({ navigation }) => {
const [game, setGame] = useState([]);
useEffect(() => {
const fetchGame = async () => {
const gameInfo = { id: '0e2cb273-b535-4cf7-ab16-198c44a4991c'};
if (!gameInfo) {
return;
}
try {
const response = await API.graphql(graphqlOperation(getGame, {id: gameInfo.id}))
setGame(response.data.getGame);
console.log(response);
} catch (e) {
}
};
fetchGame();
}, [])
return (
<SafeAreaView>
<View>
<Text>
{game.name}
</Text>
</View>
<SectionList
sections={game.sections.items}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<View>
<Text>{title}</Text>
</View>
)}>
</SafeAreaView>
)
};
export default GameScreen;
Log of the object.
I am attempting to display the getGame.sections.items array but am returning an error undefined is not an object. Cannot read property items of undefined.
Please help, I am so stumped now. When I call game.name earlier in the function it displays correctly, but game.sections.items throws an error in the SectionList that it is undefined.
Xadm, you pointed me in the right direction. I added this to my code:
const [game, setGame] = useState({});
const [gameSection, setGameSection] = useState([]);
and in my useEffect:
setGameSection(response.data.getGame.sections.items)
When calling the data, game.name wanted an object, while game.sections.items wanted an array for the SectionList. Adding 2 different functions for each initial states, one for the objects and one for the array, was able to fix the problem and render the data.

Why useEffect is triggering without dependency change?

I wanted only when I setCart to trigger useEffect. But this is not happening:
import React from 'react'
import { View } from 'react-native'
const CartScreen = () => {
const [cart, setCart] = React.useState([])
React.useEffect(() => {
console.log('test');
}, [cart])
return (
<View>
</View>
)
}
export default CartScreen;
Output: test
it fires without even having touched the cart state
useEffect will always run the first time when your component is rendered. If you only want some code to run after you change the state you can just have an if statement to check that
import React from 'react'
import { View } from 'react-native'
const CartScreen = () => {
const [cart, setCart] = React.useState([])
React.useEffect(() => {
if(cart.length > 0)
console.log('test')
}, [cart])
return (
<View>
</View>
)
}
export default CartScreen;

NavigationEvents is not working when use going back

I am building a small sound player page. I am using expo-av library.
I got noticed when the user going forward {NavigationEvents onWillBlur } is working and when he goes backward it's not executing.
What I need to reach are :
1) Stop sound playing when the user leave page either backward or forward.
2) If user presses play twice the sound is being played twice so I don't want it to be played again if it's already running
If there is any other library could be use instead of expo-av ?
import React, {useState} from 'react';
import {View, Text, Button, StyleSheet, TouchableOpacity } from 'react-native';
import { NavigationEvents } from 'react-navigation';
import { Audio } from 'expo-av';
import {AntDesign, Entypo} from '#expo/vector-icons';
const PlaySound = ({link}) => {
const [error, setError] = useState('')
const soundObject = new Audio.Sound();
const mySound = async () => {
try {
await soundObject.loadAsync({ uri : link });
await soundObject.playAsync();
} catch (err) {
setError('Wait while uploading your sound');
}
}
const stopSound = async () => {
try {
await soundObject.stopAsync(mySound);
} catch (error) {
setError('You must Play Sound First')
}
}
const pause = async () => {
try {
await soundObject.pauseAsync(mySound);
} catch (error) {
setError('Something went wrong !!! Please try again');
}
}
return (
<View>
<NavigationEvents onWillBlur = {stopSound} />
<Text>Play Sound</Text>
<View style = {styles.row}>
<TouchableOpacity
onPress = {mySound}>
<AntDesign name = 'caretright' size = {25} />
</TouchableOpacity>
<TouchableOpacity
onPress = {stopSound} >
<Entypo name = 'controller-stop' size = {25}/>
</TouchableOpacity>
<TouchableOpacity
onPress = {pause}>
<AntDesign name = 'pause' size = {25} />
</TouchableOpacity>
</View>
{error ? <Text>{error} </Text> : null }
</View>
);
};
const styles = StyleSheet.create({
row : {
flexDirection : 'row',
justifyContent : 'space-between',
marginVertical : 10
}
});
export default PlaySound;
For the problem 1 in which you have to stop player when user leaves the page. You can use useEffect hook. It will be something like that,
useEffect(() => {
return () => {
stopSound();
}
}, []);
So in the above useEffect hook, the returned function will run when component will unmount from screen (forward or backward).
For the 2nd problem, you have to disable play button to avoid multiple clicks. You can create a state using useState hook and make it false on Play button click and pass this playButtonState to disable prop of Play Button Touchable Opacity.
I hope it's clear to you now.

can react-native-root-siblings work with react-redux

in a handleClick function, update the rootSiblings like this,
handleClick() { this.progressBar.update( <ProgressBar /> ); }
and in ProgressBar component,
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { View } from 'react-native';
const getFinishedWidth = progress => ({ width: progress * totalWidth });
const getUnfinishedWidth = progress => ({ width: (1 - progress) * totalWidth });
function CustomerReassignProgressBar(props) {
const { progress } = props;
return (
<View style={styles.bar}>
<View style={getFinishedWidth(progress)} />
<View style={getUnfinishedWidth(progress)} />
</View> );
}
CustomerReassignProgressBar.propTypes = { progress: PropTypes.number, };
const mapStateToProps = state => ({ progress: state.batchReassignProgress, });
export default connect(mapStateToProps)(ProgressBar);
then, when calling handleClick(), the app crushed, the error is, 'Could not find "store" in either the context or props of "Connect(ProgressBar)". Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(ProgressBar)".'
if I don't use connect in component, it works well. So, I guess, maybe rootSiblings can not work with react-redux. But does anyone knows this problem?
Upgrade to react-native-root-siblings#4.x
Then
import { setSiblingWrapper } from 'react-native-root-siblings';
import { Provider } from 'react-redux';
const store = xxx;// get your redux store here
// call this before using any root-siblings related code
setSiblingWrapper(sibling => (
<Provider store={store}>{sibling}</Provider>
));