Invalid Hook Call For React Native - javascript

I am having an Invalid Hook Error in RN. I am using a button click event handler to execute a setInterval function for a countdown timer.
Error: 'Hooks can only be called inside the body of a function component. (...)'
My code:
import { Button, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import React, { useEffect, useState } from 'react'
import { Ionicons } from '#expo/vector-icons'
import { AntDesign } from '#expo/vector-icons'
export default function MenuBar() {
const [time, SetTime] = useState(10);
const startTime = () => {
useEffect(() => {
const interval = setInterval(() => {
if(time > 0) {
SetTime(time => time - 1);
} else {
SetTime(time => time + 10);
}
}, 1000);
return () => clearInterval(interval);
}, []);
}
return (
<View style={styles.container}>
<Button color="orange" onPress={startTime} title="Start Time!!"></Button>
<View style={styles.menu}>
<TouchableOpacity>
<AntDesign style={[styles.button, styles.exitBtn] } name="logout" size={24} color="white" />
</TouchableOpacity>
<TouchableOpacity>
<AntDesign style={styles.button} name="questioncircleo" size={24} color="white" />
</TouchableOpacity>
<Text style={styles.timer}>{time}</Text>
<TouchableOpacity>
<AntDesign style={styles.button} name="picture" size={24} color="white" />
</TouchableOpacity>
<TouchableOpacity>
<AntDesign style={styles.button} name="sound" size={24} color="white" />
</TouchableOpacity>
</View>
</View>
)
}

You cannot call a hook inside of another function unless that function is a React Component.
As you want to start the timer when pressing a button you don't need to listen to side effects and therefore don't need to call useEffect, and just start the timer when pressing the button.
You do need to clear the timer when unmounting the component. For this you will need a useEffect, as React internally will trigger the useEffect cleanup function.
I would suggest something like this:
export default function MenuBar() {
const interval = useRef(null)
const [time, setTime] = useState(10);
const startTime = () => {
if (interval.current) {
// Making sure not to start multiple timers if one
// has already started
clearInterval(interval.current);
}
interval.current = setInterval(() => {
if (time > 0) {
setTime(time => time - 1);
} else {
setTime(time => time + 10);
}
}, 1000);
}
// only use useEffect when unmounting the component
// and calling the cleanup function
useEffect(() => {
return () => {
if (interval.current) {
return clearInterval(interval.current);
}
};
}, []);
return (
// rest of component
)

Related

Can setInterval block user input?

I tried to program a little stopwatch to test something out but after clicking "Start" and its running the "Stop", "Lap" and "Reset" Buttons register the input up to a second or more after I click them. What am I missing here?
My guess is it has something to do with the useEffect hook, but Im not sure since I haven't used React or React Native that extensively.
export default function TabOneScreen({ navigation }: RootTabScreenProps<'TabOne'>) {
const [time, setTime] = useState<number>(0);
const [timerOn, setTimerOn] = useState(false);
const [lapCounter, setLapCounter] = useState<number>(0);
const [laps, setLaps] = useState<{count: number, lapTime: number}[]>([])
useEffect(() => {
var interval: any = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 10);
}, 10);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
return (
<View style={styles.container}>
<Text>time:</Text>
<View style={styles.timer}>
<Text>{("0" + Math.floor(time / 60000) % 60).slice(-2)}:</Text>
<Text>{("0" + Math.floor(time / 1000) % 60).slice(-2)}:</Text>
<Text>{("0" + (time / 10) % 100).slice(-2)}</Text>
</View>
<View style={styles.buttons}>
<Button
title="Start"
onPress={() => setTimerOn(true)}
/>
<Button
title="Stop"
onPress={() => setTimerOn(false)}
/>
<Button
title="Lap"
onPress={() => {
setLapCounter(counter => counter += 1)
setLaps(prevLaps => [
...prevLaps,
{count: lapCounter, lapTime: time}
]
)
}}
/>
<Button
title="Reset"
onPress={() => {
setTimerOn(false)
setTime(0)
setLapCounter(0)
setLaps([])
}
}
/>
</View>
<FlatList
data={laps}
renderItem={({ item }) =>
<View style={styles.lapList}>
<Text style={styles.item}>Lap: {item.count}</Text>
<Text style={styles.item}>{item.lapTime}</Text>
</View>
}
/>
</View>
);
}
On the "else if" you clear an empty interval (because you did not save the previous one anywhere). Create a new hook, such as useTimer.
Or use a premade like: https://www.npmjs.com/package/use-timer

Callback function leads to infinity loop error in react native

I am new to react native and simply can not find the reason for the infinity loop error I spent my last few hours with... Here is what is happening:
I have the following custom component
import { TouchableOpacity, StyleSheet, View, Text } from "react-native";
import { FontAwesome } from "#expo/vector-icons";
function AnswerContainer_CheckBox(props) {
const [checked, setChecked] = useState(false);
const checkedHandler =()=>{
if (checked == false) {
setChecked(true);
} else {
setChecked(false);
}
}
return (
<View>
<TouchableOpacity
onPress={() => {
checkedHandler();
props.true_1;
}
}
>
<View style={styles.mainContainer}>
<View style={styles.icon}>
<FontAwesome
name={checked == true ? "check-square" : "square-o"}
size={24}
color={checked == true ? "#3787FF" : "#BFD3E5"}
/>
</View>
<Text style={styles.checkButtonText}>{props.title}</Text>
</View>
</TouchableOpacity>
</View>
);
}
I use that Component in my screen like this:
function myScreen(props) {
const [true1, setTrue1] = useState(false);
const setTrue_1_Handler = () => {
switch (true1) {
case false:
setTrue1(true);
alert("true");
break;
case true:
setTrue1(false);
break;
}
};
return (
<SafeAreaView>
<AnswerContainer_CheckBox true_1={setTrue_1_Handler()} title="Test_1" />
<AnswerContainer_CheckBox title="Test_2" />
<AnswerContainer_CheckBox title="Test_3" />
</SafeAreaView>
);
}
Whenever I navigate to "myScreen", the infite loop error apperas and crashes the app - even though I didn't press that button yet.
Any ideas? I know the issue has come up a few times but I still didn't make it work (useEffect didn't help somehow...).
Thanks

Dynamically render component for each item in array React Native

Beginner question here, not sure exactly what this would be considered, but I'm trying to make a form where a user can add and remove input rows upon pressing a button. What do I need to change to render new components or remove components when the Add or Remove buttons are pressed? Right now, the Add and Remove button change the textInput array appropriately, but components are not actually being added or removed.
Here is my current code:
FormScreen.js
import React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { Button, Caption } from 'react-native-paper';
import InputCard from '../components/InputCard';
const FormScreen = props => {
const textInput = [1,2,3];
const addTextInput = () => {
let currArr = textInput;
let lastNum = currArr[currArr.length -1]
let nextNum = lastNum + 1
console.log(currArr, lastNum, nextNum);
textInput.push(
nextNum
);
console.log(textInput);
};
const removeTextInput = () => {
textInput.pop();
console.log(textInput);
};
return (
<ScrollView>
<View style={styles.col}>
<View style={styles.row}>
<Caption>Favorite colors?</Caption>
</View>
<View style={styles.row}>
<View>
{textInput.map(key => {
return (
<InputCard key={key}/>
);
})}
</View>
</View>
<View>
<View style={styles.col}>
<Button title='Add' onPress={() => addTextInput()}>Add</Button>
</View>
<View style={styles.col}>
<Button title='Remove' onPress={() => removeTextInput()}>Remove</Button>
</View>
</View>
</View>
</ScrollView>
);
};
export default FormScreen;
InputCard.js
import React, { useState } from "react";
import { View, StyleSheet } from 'react-native';
import { Caption, Card, TextInput } from "react-native-paper";
const InputCard = (props) => {
const [input, setInput] = useState('');
return (
<View>
<Card>
<Card.Content>
<Caption>Item {props.key}</Caption>
<View style={styles.row}>
<View style={styles.half}>
<TextInput
label="Input"
value={input}
onChangeText={input => setInput(input)}
mode="outlined"
style={styles.textfield}
/>
</View>
</View>
</Card.Content>
</Card>
</View>
);
}
export default InputCard;
Instead of storing it in a array, try to do something like this, using 2 states.
const [totalTextInput, setTotalTextInput] = useState([])//initial state, set it to any data you want.
const [count, setCount] = useState(0);
const addTextInput = () => {
setCount((prevState) => prevState + 1);
setTotalTextInput((prevState) => {
const newTextInput = Array.from(prevState); // CREATING A NEW ARRAY OBJECT
newTextInput.push(count);
return newTextInput;
});
};
const removeTextInput = () => {
setTotalTextInput((prevState) => {
const newTextInput = Array.from(prevState); // CREATING A NEW ARRAY OBJECT
newTextInput.pop();
return newTextInput;
});
};
And in your code:
<View>
{totalTextInput.map(key => {
return (
<InputCard key={key}/>
);
})}
</View>

useState is not working properly in React Native

I'm working on an application and using WordPress API for showing posts. I've created 2 buttons to navigate the list of posts. As you know there is an argument "page=" to get posts on a specific page, I've initialized a state to maintain page number. The main problem is that it's not incrementing correctly.
Post Screen Code -
import React, { useState, useEffect } from "react";
import { View, FlatList, TouchableOpacity } from "react-native";
import { Colors } from "../constant/colors";
import globalStyles from "../constant/globalStyle";
import axios from "axios";
import PostCard from "../components/PostCard";
import CustomButton from "../components/Button";
const Updates = () => {
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const response = await axios.get(
`https://bachrasouthpanchayat.in/wp-json/wp/v2/posts?embed=true&page=${page}`
);
setData(response.data);
setLoaded(true);
};
const previousHandler = () => {
setLoaded(false);
let newPage = page - 1;
setPage(newPage);
fetchData();
};
const nextHandler = () => {
setLoaded(false);
let newPage = page + 1;
setPage(newPage);
fetchData();
};
return (
<View
style={{
...globalStyles.container,
backgroundColor: Colors.BACKGROUND_SCREEN,
}}
>
{loaded ? (
<>
<FlatList
style={{ flex: 1, margin: 10 }}
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<TouchableOpacity activeOpacity={0.7}>
<PostCard
title={item.title.rendered}
imageUrl={item.jetpack_featured_media_url}
/>
</TouchableOpacity>
);
}}
/>
<View
style={{
flexDirection: "row",
alignItems: "center",
alignContent: "stretch",
justifyContent: "center",
}}
>
{page == 1 ? (
<TouchableOpacity
activeOpacity={0.7}
style={{ width: "100%" }}
onPress={nextHandler}
>
<CustomButton>Next</CustomButton>
</TouchableOpacity>
) : (
<>
<TouchableOpacity
activeOpacity={0.7}
style={{ marginRight: 2, width: "50%" }}
onPress={previousHandler}
>
<CustomButton>Previous</CustomButton>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
style={{ width: "50%" }}
onPress={nextHandler}
>
<CustomButton>Next</CustomButton>
</TouchableOpacity>
</>
)}
</View>
</>
) : null}
</View>
);
};
export default Updates;
I had logged state in every state and found that was not incrementing from 1 to 2 on pressing the button the first time. I think state updated after API call because both buttons had started showing even I've used condition to show both buttons only if the state is not 1
Please let me know if i've made any silly mistake 😂😂
useEffect(() => {
fetchData();
}, []);
The last argument to useEffect is an array of dependencies so that React will only re-run the effect when the dependencies have changed. You are passing an empty array, which tells React that there are no dependencies and the effect should only be run once, when the component is first mounted.
Now you actually want the effect to re-run when the page changes, so you should put page in the depenency array:
useEffect(() => {
fetchData();
}, [page]);
And (credit: #Keith) you should remove the extra fetchData() calls in the nextHandler and previousHandler
You have to implement it like this:
useEffect(() => {
const fetchData = async () => {
setLoaded(true);
const response = await axios.get(
`https://bachrasouthpanchayat.in/wp-json/wp/v2/posts?embed=true&page=${page}`
);
setData(response.data);
setLoaded(false);
};
fetchData();
}, [page]);
const previousHandler = () => {
setPage(prevPage => prevPage - 1);
};
This way whenever the user changes the page, it will automatically call the function in useEffect, since it is in the dependency array.

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

Categories

Resources