React native useEffect clean up not using the updated states properly - javascript

I have been scratching my head on something weird that happens during the clean up function of useEffect. Basically, in the clean up code, I invoke a updateStore() function to update store based on the boolean value of changeFlag. However, the updateStore() and even the clean up function itself is not reflecting the updated state of changeFlag and I could not figure out why.
I tried to do a simple POC on a clean project thinking that it might be caused by some bug of my project but it is still having the same problem.
App.js
import React, {useState, useEffect} from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
const Test = ({navigation}) => {
//const { id } = route.params;
const [changeFlag, setChangeFlag] = useState(false);
useEffect(() => {
console.log(`Start: ${changeFlag}`);
return function cleanup () {
console.log(`Exiting: ${changeFlag}`);
updateStore();
};
}, []);
useEffect(() => {
console.log(`Flag changed! ${changeFlag}`);
})
const toggle = () => {
setChangeFlag(!changeFlag);
}
const updateStore = () => {
console.log(`Print flag ${changeFlag}`);
}
return (
<View style={styles.container}>
<Text>{changeFlag ? "True" : "False"} </Text>
<Button title={"Press toggle"} onPress={toggle}/>
</View>
);
}
const Home = ({navigation}) => {
return(
<View>
<Button title={"Go Test"} onPress={() => navigation.navigate('Test')}/>
</View>
);
}
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Test" component={Test} />
</Stack.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
export default App
Output
Start: false
Flag changed! false
Flag changed! true
Exiting: false
Print flag false
Thanks in advance!

If you want to have access to the new value of changeFlag in useEffect you need to put it in the [] at the end of the useEffect, by doing this you're saying the useEffect to keep track of the updated value
useEffect(() => {
console.log(`Start: ${changeFlag}`);
return function cleanup () {
console.log(`Exiting: ${changeFlag}`);
updateStore();
};
}, [changeFlag]);

Related

Button that leads to another screen/page in react native

i'am fairly new to react native and i'm doing a social media clone. I've got my navigator and the login via a database done , but i was wondering if there was a way to link a page/screen to another one by pressing a button .
Here is my code for the home screen after logging in :
import React, {Component} from 'react';
import {View, Text, FlatList} from 'react-native';
import AsyncStorage from '#react-native-async-storage/async-storage';
class HomeScreen extends Component {
constructor(props){
super(props);
this.state = {
isLoading: true,
listData: []
}
}
componentDidMount() {
this.unsubscribe = this.props.navigation.addListener('focus', () => {
this.checkLoggedIn();
});
this.getData();
}
componentWillUnmount() {
this.unsubscribe();
}
getData = async () => {
const value = await AsyncStorage.getItem('#session_token');
return fetch("http://localhost:3333/api/1.0.0/search", {
'headers': {
'X-Authorization': value
}
})
.then((response) => {
if(response.status === 200){
return response.json()
}else if(response.status === 401){
this.props.navigation.navigate("Login");
}else{
throw 'Something went wrong';
}
})
.then((responseJson) => {
this.setState({
isLoading: false,
listData: responseJson
})
})
.catch((error) => {
console.log(error);
})
}
checkLoggedIn = async () => {
const value = await AsyncStorage.getItem('#session_token');
if (value == null) {
this.props.navigation.navigate('Login');
}
};
render() {
if (this.state.isLoading){
return (
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Loading..</Text>
</View>
);
}else{
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}}>
<Text>Welcome to The app </Text>
</View>
);
}
}
}
export default HomeScreen;
Now ideally i would want a button in my else statement which could lead me to another screen (eg main screen of the app ) after logging in .
App.js :
import 'react-native-gesture-handler';
import React, { Component } from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createDrawerNavigator } from '#react-navigation/drawer';
import HomeScreen from './screens/home';
import LoginScreen from './screens/login';
import SignupScreen from './screens/signup';
import LogoutScreen from './screens/logout';
const Drawer = createDrawerNavigator();
class App extends Component{
render(){
return (
<NavigationContainer>
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Login" component={LoginScreen} />
<Drawer.Screen name="Signup" component={SignupScreen} />
<Drawer.Screen name="Logout" component={LogoutScreen} />
</Drawer.Navigator>
</NavigationContainer>
);
}
}
export default App;
If you're new to react native, I'd suggest that you give functional components and hooks a try, this is how your code would look like as a functional component after some clean up
(The answer is included in component)
import React, { Component, useEffect, useState } from "react";
import { View, Text, FlatList, Button } from "react-native";
import AsyncStorage from "#react-native-async-storage/async-storage";
const HomeScreen = (props) => {
const [isLoading, setIsLoading] = useState(true);
const [listData, setListData] = useState([]);
useEffect(() => {
const subscription = props.navigation.addListener("focus", () => {
checkLoggedIn();
});
getData();
return subscription.remove(); //similar to component unmount, syntax might be different based on your version
}, []);
const getData = async () => {
setIsLoading(true)
const value = await AsyncStorage.getItem("#session_token");
return fetch("http://localhost:3333/api/1.0.0/search", {
headers: {
"X-Authorization": value,
},
})
.then((response) => {
if (response.status === 200) {
return response.json();
} else if (response.status === 401) {
props.navigation.navigate("Login");
} else {
throw "Something went wrong";
}
})
.then((responseJson) => {
setIsLoading(false);
setListData(responseJson);
})
.catch((error) => {
console.log(error);
});
};
const checkLoggedIn = async () => {
const value = await AsyncStorage.getItem("#session_token");
if (value == null) {
props.navigation.navigate("Login");
}
};
return (
<>
{isLoading ? (
<View
style={{
flex: 1,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Loading..</Text>
</View>
) : (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Welcome to The app </Text>
<Button
title="Next Page"
onPress={() => {
props.navigation.navigate("yourScreenName");
}}
/>
</View>
)}
</>
);
};
export default HomeScreen;
since you screens are
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Login" component={LoginScreen} />
<Drawer.Screen name="Signup" component={SignupScreen} />
<Drawer.Screen name="Logout" component={LogoutScreen} />
//you can add more screens here just make sure to import them first
//example
<Drawer.Screen name="Example" component={ExampleScreen} />
// name will be the name you use to navigate to that screen
// component should have the screen you imported
then you should pass one of those names to props.navigation.navigate() so the button would be
<Button
title="Next Page"
onPress={() => {
props.navigation.navigate("Login");
}}
/>
If you want to stick with a class component, then :
import {TouchableOpacity} from "react-native"
In the else part,
<TouchableOpacity style ={{width:100, height:100}} onPress={()=>{this.props.navigation.navigate("screenname")}}></TouchableOpacity>
TouchableOpacity is a blank space on the screen, which can be styled easily, and you have an onPress event. Basically it's like a better version of a button. You can also place other components inside the TouchableOpacity, and make the whole thing clickable

Too many re-renders on search API

I am new to this so I hope this is the right place to get help!
As titled, executing this code is giving me the "Too many re-renders" error on React.
I have tried going through all lines and checking my hooks repeatedly, but nothing seems to work.
I am guessing this is happening due to useEffect, so pasting the code for the relevant components below:
UseResults:
import { useEffect, useState } from 'react';
import yelp from '../api/yelp';
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState('');
const searchApi = async () => {
try {
const response = await yelp.get('/search', {
params: {
limit: 50,
term,
location: 'san francisco'
}
});
setResults(response.data.businesses);
} catch (err) {
setErrorMessage('Something went wrong')
}
};
useEffect(() => {
searchApi('pasta');
}, []);
return [searchApi, results, errorMessage];
}
SearchScreen:
import React, { useState } from 'react';
import { Text, StyleSheet } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import ResultsList from '../components/ResultsList';
import SearchBar from '../components/SearchBar';
import useResults from '../hooks/useResults';
const SearchScreen = (navigation) => {
const [term, setTerm] = useState('');
const [searchApi, results, errorMessage] = useResults();
const filterResultsByPrice = (price) => {
return results.filter(result => {
return result.price === price;
});
};
return <>
<SearchBar
term={term}
onTermChange={setTerm}
onTermSubmit={searchApi()}
/>
{errorMessage ? <Text>{errorMessage}</Text> : null}
<Text>We have found {results.length} results</Text>
<ScrollView>
<ResultsList
results={filterResultsByPrice('$')}
title="Cost Effective"
navigation={navigation}
/>
<ResultsList
results={filterResultsByPrice('$$')}
title="Bit Pricier"
navigation={navigation}
/>
<ResultsList
results={filterResultsByPrice('$$$')}
title="Big Spender"
navigation={navigation}
/>
</ScrollView>
</>
};
const styles = StyleSheet.create({});
export default SearchScreen;
ResultsList:
import React from 'react';
import { View, Text, StyleSheet, FlatList } from 'react-native';
import { TouchableOpacity } from 'react-native-gesture-handler';
import ResultsDetail from './ResultsDetail';
const ResultsList = ({ title, results, navigation }) => {
return (
<View style={styles.container} >
<Text style={styles.title}>{title}</Text>
<FlatList
horizontal
showsHorizontalScrollIndicator={false}
data={results}
keyExtractor={result => result.id}
renderItem={({ item }) => {
return (
<TouchableOpacity onPress={() => navigation.navigate('ResultsShow')}>
<ResultsDetail result={item} />
</TouchableOpacity>
)
}}
/>
</View>
);
};
const styles = StyleSheet.create({
title: {
fontSize: 18,
fontWeight: 'bold',
marginLeft: 15,
marginBottom: 5
},
container: {
marginBottom: 10
}
});
export default ResultsList;
TIA!

React Native ref.injectJavascript on WebView always undefined

I have a simple functional component with a WebView inside a TabNavigator where I'd like to inject some javascript when the tab is focused like so:
import React, {useRef, useEffect} from 'react';
import {WebView} from 'react-native-webview';
export default (props) => {
const webViewRef = useRef();
React.useEffect(() => {
const unsubscribe = props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript('console.log("Hello World")');
});
return unsubscribe;
}, []);
return (
<WebView ref={webViewRef} />
);
}
For some reason, webViewRef is always undefined in the listener.
If I do something like <WebView ref={(r) => console.log(r)} /> I get something like the following:
EDIT:
Simply adjusting the provided example -does- work, so I suppose I will have to figure out what is going on in my code:
import React, { useRef } from 'react'
import { View } from 'react-native'
import { WebView } from 'react-native-webview'
export default () => {
const webViewRef = useRef(null);
const run = `
document.body.style.backgroundColor = 'blue';
true;
`
setTimeout(() => {
webViewRef.current.injectJavaScript(run)
}, 3000)
return (
<View style={{ flex: 1 }}>
<WebView
ref={webViewRef}
source={{
uri:
'https://github.com/react-native-community/react-native-webview',
}}
/>
</View>
)
}
EDIT 2: Adjusting the simple example and attempting to use the ref inside the listener does not work:
import React, { useRef } from 'react'
import { View } from 'react-native'
import { WebView } from 'react-native-webview'
export default (props) => {
const webViewRef = useRef(null);
const run = `
document.body.style.backgroundColor = 'blue';
true;
`
const handleLoadEnd = () => {
props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript(run)
})
}
return (
<View style={{ flex: 1 }}>
<WebView
ref={webViewRef}
source={{
uri:
'https://github.com/react-native-community/react-native-webview',
}}
onLoadEnd={handleLoadEnd}
/>
</View>
)
}
Edit 3: useFocusEffect also has same problem:
useFocusEffect(
React.useCallback(() => {
webViewRef.current.injectJavascript('alert("HI")')
}, [props.navigation])
)
Sources:
React Navigation: Call a function when focused screen changes
React Native WebView: Communicating between JS and Native
Oh man, it's really another one of those epic hair pulling days:
webViewRef.injectJavascript < BAD
webViewRef.injectJavaScript < GOOD
Somebody please give the last 4 hours of my life back
The ref will be set, if the onLoadEnd event has triggered. So you must adjust your code like so:
export default (props) => {
const webViewRef = useRef();
const handleLoadEnd = () => {
props.navigation.addListener('focus', () => {
webViewRef.current.injectJavascript('console.log("Hello World")');
});
}
return (
<WebView ref={webViewRef} onLoadEnd={handleLoadEnd}/>
);
}
I have a working example. See here https://github.com/Tracer1337/MRGVP/blob/master/vertretungsplan/components/PaginatedWebview/PaginatedWebview.js

prevent backhandler from minimizing the app

how can I prevent the app from minimizing / exiting when pushing back button on my device?
Im trying to assign "browsers back" functionality when pressing back button on my device, heres my code:
import 'react-native-get-random-values';
import React, { useState, useRef, Component, useEffect } from 'react'
import {
Alert,
SafeAreaView,
StyleSheet,
StatusBar,
View,
Text,
ScrollView,
BackHandler,
RefreshControl
} from 'react-native'
import WebView from 'react-native-webview'
import Icon from 'react-native-vector-icons/FontAwesome';
import { Button } from 'react-native-elements';
const App = () => {
function backButtonHandler(){}
function refreshHandler(){
if (webviewRef.current) webviewRef.current.reload()
}
useEffect(() => {
BackHandler.addEventListener("hardwareBackPress", backButtonHandler);
return () => {
BackHandler.removeEventListener("hardwareBackPress", backButtonHandler);
};
}, [backButtonHandler]);
let jscode = `window.onscroll=function(){window.ReactNativeWebView.postMessage(document.documentElement.scrollTop||document.body.scrollTop)}`;
const [canGoBack, setCanGoBack] = useState(false)
const [currentUrl, setCurrentUrl] = useState('')
const [refreshing, setRefreshing] = useState(false);
const [scrollviewState, setEnableRefresh] = useState(false);
const webviewRef = useRef(null)
const scrollviewRef = useRef(false)
backButtonHandler = () => {
if (webviewRef.current) webviewRef.current.goBack()
}
return (
<>
<StatusBar barStyle='dark-content' />
<SafeAreaView style={styles.flexContainer}>
<ScrollView
contentContainerStyle={styles.flexContainer}
refreshControl={
<RefreshControl refreshing={false} onRefresh={refreshHandler} ref={scrollviewRef} enabled={ (scrollviewState) ? true : false } />
}>
<WebView
source={{ uri: 'https://youtube.com' }}
startInLoadingState={true}
ref={webviewRef}
onNavigationStateChange={navState => {
setCanGoBack(navState.canGoBack)
setCurrentUrl(navState.url)
}}
injectedJavaScript={jscode}
onMessage={(event)=>{
let message = event.nativeEvent.data;
let num = parseInt(message);
if(num==0){setEnableRefresh(true)}
else{setEnableRefresh(false)}
}}
/>
</ScrollView>
<View style={styles.tabBarContainer}>
<Button onPress={backButtonHandler}
icon={
<Icon
name="arrow-left"
size={15}
color="white"
/>
}
containerStyle={styles.buttonx}
title="Back"
/>
</View>
</SafeAreaView>
</>
)
}
const styles = StyleSheet.create({
flexContainer: {
flex: 1
},
tabBarContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: 'orange'
},
buttonx: {
backgroundColor:'blue',
width:'100%'
}
})
export default App
my problem is when I press the back button on my device, it does call backButtonHandler function and my webview navigates back, but at the same time the app minimizes too.. is there a way to prevent this?
change your backButtonHandler Method to just return true, when your backHandler Method does return true, it actually does nothing onPress Back button :
backButtonHandler = () => {
return true;
}

React Native Delete Function not working as intended

The intended function is that it would just delete the one goal that is clicked but for some odd reason it decides onPress to delete all goals listed.
I am following this tutorial https://www.youtube.com/watch?v=qSRrxpdMpVc and im stuck around 2:44:45. If anyone else has done this tutorial and or can see my problem an explanation would be greatly appreciated. :)
Program
import React, { useState } from "react";
import {
StyleSheet,
Text,
View,
Button,
TextInput,
ScrollView,
FlatList
} from "react-native";
import GoalItem from "./components/GoalItem";
import GoalInput from "./components/GoalInput";
export default function App() {
const [courseGoals, setCourseGoals] = useState([]);
const addGoalHandler = goalTitle => {
setCourseGoals(currentGoals => [
...currentGoals,
{ key: Math.random().toString(), value: goalTitle }
]);
};
const removeGoalHander = goalId => {
setCourseGoals(currentGoals => {
return currentGoals.filter((goal) => goal.id !== goalId);
});
};
return (
<View style={styles.screen}>
<GoalInput onAddGoal={addGoalHandler} />
<FlatList
keyExtractor={(item, index) => item.id}
data={courseGoals}
renderItem={itemData => (
<GoalItem
id={itemData.item.id}
onDelete={removeGoalHander}
title={itemData.item.value}
/>
)}
></FlatList>
</View>
);
}
const styles = StyleSheet.create({
screen: {
padding: 80
}
});
Function
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
const GoalItem = props => {
return (
<TouchableOpacity onPress={props.onDelete.bind(this, props.id)}>
<View style={styles.listItem}>
<Text>{props.title}</Text>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
listItem: {
padding: 10,
backgroundColor: "lightgrey",
borderColor: "grey",
borderRadius: 5,
borderWidth: 1,
marginVertical: 10
}
});
export default GoalItem;
When updating state, you have to pass new array so component can detect changes and update view
const removeGoalHander = goalId => {
setCourseGoals(currentGoals => {
const newGoals = currentGoals.filter((goal) => goal.id !== goalId);
return [...newGoals];
});
};

Categories

Resources