I'm trying to make a slideshow where the user can play them in a default or random order and cycle through them by pressing a button. The problem comes when the user has chosen random mode; pressing the button still increments the currentIndex variable, but for some reason images[currentIndex] does not change.
const SlideshowScreen = (props) => {
const settings = props.route.params.settings;
const [images, setImages] = useState(settings.random ? props.route.params.images.sort((a, b) => 0.5 - Math.random()) : props.route.params.images);
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
console.log('currentIndex', currentIndex, images[currentIndex]);
}, [currentIndex])
const newImage = () => {
if (currentIndex + 1 >= images.length) {
console.log('done');
}
else {
setCurrentIndex(currentIndex + 1);
}
}
return (
<View style={styles.mainContainer}>
<View style={styles.imageContainer}>
<Image
style={styles.image}
source={images[currentIndex].path} key={images[currentIndex].id} />
</View>
<TouchableOpacity onPress={() => newImage()}>
<Text>Next image</Text>
</TouchableOpacity>
</View>
)
}
On some clicks of the button this happens, and on others it doesn't. How is this happening? Any help would be great, thanks.
Related
Problem:
When using a ScrollView in ReactNative for horizontal pagination it re-renders all children, but I would like to keep the state values of certain local input fields and local variables of children components.
In the code below, if I were in the middle of updating a TextInput within the NotesSection but wanted to swipe back to the BatchSection to review some metadata, the code re-renders NotesSection and resets a local state holding the text value.
Diagnosis:
I'm very new to React and React Native, but my best guess here is that this happens due to the parent state variable "horizontalPos" which takes an integer to reflect what page is in focus.
This is simply used in the ProductHeader component to highlight a coloured bottomBorder showing the user a kind of small "menu" at the top of the screen.
The "horizontalPos" state can be updated in 2 ways:
First one is simply when clicking the wanted header (TouchableOpacity) within ProductHeader which triggers a state change and uses useRef to automatically move the ScrollView.
Second option is when the user swipes on the ScrollView. Using OnScroll to run a function "handleHorizontalScroll" which in turn sets the "horizontalPos" state using simple maths from the contentOffset.x.
Question / Solution:
If "horizontalPos" state was INSIDE ProductHeader I suspect this would solve the issue but I can't wrap my mind around how to do this as I don't believe it's possible to pass a function through to the child based on a change in the parent component.
I'm dependent on registering the OnScroll on the main ScrollView and the remaining components likewise have to be inside the main ScrollView but I don't want them to re-render every time the "horizontalPos" state updates.
Code:
const ProductScreen = (props) => {
const [horizontalPos, setHorizontalPos] = useState(0)
const scrollRef = useRef()
const toggleHorizontal = (page) => {
setHorizontalPos(page)
scrollRef.current.scrollTo({x:page*width, y:0, animated:false})
}
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width)
if (pagination != horizontalPos){
setHorizontalPos(pagination)
}
}
const ProductHeader = () => {
return(
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 0 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(0)}>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.scrollHeader, horizontalPos == 1 ? {borderColor: AppGreenDark,} : null]} onPress={() => toggleHorizontal(1)}>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
)
}
return (
<View style={styles.container}>
<ProductHeader/>
<ScrollView
ref={scrollRef}
decelerationRate={'fast'}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={handleHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{flex: 1}}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection/>
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection/>
</ScrollView>
</View>
</ScrollView>
</View>
)
}
As you outlined, updating horizontalPos state inside ProductScreen will cause a whole screen to re-render which is not an expected behavior.
To avoid this scenario, let's refactor the code as below:
function debounce(func, timeout = 500){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
class ProductHeader extends React.Component {
state = {horizontalPos:0 }
toggleHorizontal = (page) => {
this.setState({horizontalPos:page});
this.props.onPositionChange(page);
};
render () {
const {horizontalPos} = this.state
return (
<View style={styles.scrollHeaderContainer}>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 0 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(0)}
>
<Text style={styles.scrollHeaderText}>Meta Data</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.scrollHeader,
horizontalPos == 1 ? { borderColor: AppGreenDark } : null,
]}
onPress={() => this.toggleHorizontal(1)}
>
<Text style={styles.scrollHeaderText}>{"Notes"}</Text>
</TouchableOpacity>
</View>
);
}
};
const ProductScreen = (props) => {
const scrollRef = useRef();
const productHeaderRef = useRef()
let horizontalPos = 0;
const handleHorizontalScroll = (v) => {
const pagination = Math.round(v.nativeEvent.contentOffset.x / width);
if (pagination != horizontalPos) {
productHeaderRef.current?.toggleHorizontal(pagination)
}
};
const debouncedHorizontalScroll= debounce(handleHorizontalScroll,500)
const onPositionChange = (page) => {
horizontalPos = page;
scrollRef.current.scrollTo({ x: page * width, y: 0, animated: false });
};
return (
<View style={styles.container}>
<ProductHeader onPositionChange={onPositionChange} ref={productHeaderRef} />
<ScrollView
ref={scrollRef}
decelerationRate={"fast"}
horizontal={true}
showsHorizontalScrollIndicator={false}
snapToInterval={width}
onScroll={debouncedHorizontalScroll}
scrollEventThrottle={16}
disableIntervalMomentum={true}
style={{ flex: 1 }}
>
<View style={[styles.horizontalScroll]}>
<View style={styles.mainScrollView}>
<BatchSection />
</View>
<ScrollView style={styles.notesScrollView}>
<NotesSection />
</ScrollView>
</View>
</ScrollView>
</View>
);
};
I hope this will stop the whole screen from rerendering and maintaining pagination.
I am trying to count the number of times a button is pressed within a second.
It's working, for the most part, it tracks it and outputs it.
But the problem is that it outputs the button press count from the last second instead of the current second.
I think it would work if the order was reversed somehow, how do I fix up this function? Thanks.
const [clicksPerSecond, setClicksPerSecond] = useState(0);
const [tempCounter, setTempCounter] = useState(0);
const [tempCounter2, setTempCounter2] = useState(0);
const { setCounter, counter } = useContext(CountContext);
useEffect(() => {
console.log(tempCounter);
if (tempCounter != 0) {
if (tempCounter == 1) {
setTimeout(() => {
setClicksPerSecond(tempCounter2);
setClicksPerMinute(tempCounter2 * 60);
setTempCounter(1);
console.log('Clicks per second final: ' + tempCounter2);
}, 1000)
} else {
setTempCounter2(tempCounter);
console.log('Clicks per second: ' + tempCounter);
}
}
setTempCounter(tempCounter + 1);
}, [counter])
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View elevation={7.5} style={{ backgroundColor: colors.background, borderRadius: 500 }}>
<TouchableOpacity
onPress={() => setCounter(counter + 1)}
style={[styles.counterButton, { backgroundColor: colors.primary, borderColor: colors.container, borderWidth: 0 }]}>
<Text></Text>
<Text style={{ fontSize: 60 }}>{counter}</Text>
<Text>{clicksPerSecond} CPS</Text>
</TouchableOpacity>
</View>
</View>
);
}
You can instead just increment the count and decrement it after a second.
export default function App() {
const [clicks, setClicks] = React.useState(0);
const onClick = () => {
setClicks((c) => c + 1);
setTimeout(() => setClicks((c) => c - 1), 1000);
};
return (
<div>
<button onClick={onClick}>Click me</button>
<p>Clicked {clicks} times</p>
</div>
);
}
You will also need to track if the component is unmounted before decrement, which I think you can do using refs. Example
I'm trying to fetch documents from a collection in Firestore and show it through a FlatList. But it shows a loading circle (IDK what it is actually called)!
I am using my own phone to test the app if it makes any difference. (I am quite new to this)
Here is a screenshot of my items screen where items are to be displayed:
Here is my code:
const Items = () => {
const [isLoading, setIsLoading] = useState(false)
const [isMoreLoading, setIsMoreLoading] = useState(false)
const [last, setLast] = useState(null)
const [items, setItems] = useState([])
let onEndReachedCalledDuringMomentum = false;
const itemsLoc = db.collection('items')
useEffect(() => {
getItems();
}, []);
getItems = async () => {
setIsLoading(true);
const snapshot = await itemsLoc.get();
if(!snapshot.empty){
let newItems = [];
setLast(snapshot.docs[snapshot.docs.length-1]);
for (let i = 0; i < snapshot.docs.length; i++){
newItems.push(snapshot.docs[i].data());
}
setItems(newItems)
}else{
setLast(null);
}
setIsLoading(false);
}
getMore = async () => {
if (last){
setIsMoreLoading(true);
let snapshot = await itemsLoc.orderBy('id').startAfter(last.data().id).limit(3).get();
if(!snapshot.empty){
let newItems = items;
setLast(snapshot.docs[snapshot.docs.length - 1]);
for (let i = 0 ; i < snapshot.docs.length; i++){
newItems.push(snapshot.docs[i].data());
}
setItems(newItems);
if(snapshot.docs.length < 3) setLast(null);
}else{
setLast(null)
}
setIsMoreLoading(false);
}
onEndReachedCalledDuringMomentum = true;
}
renderList = ({name,desc,image}) => {
return(
<View style={styles.container}>
<Image source={{uri: image}} style={styles.imageContainer}/>
<View style={styles.itemInfoContainer}>
<View>
<Text style={styles.title}>{name}</Text>
</View>
<View>
<Text style={styles.description}>{desc}</Text>
</View>
</View>
</View>
)
}
renderFooter = () => {
if (isMoreLoading) {return true;}
return <ActivityIndicator size="large" color="#64aeae" style={{ marginBottom:10 }}/>
}
onRefresh = () => {
getItems();
}
return(
<View style={{marginTop: 20}}>
<FlatList
showsVerticalScrollIndicator={false}
data={items}
keyExtractor={item => item.id}
renderItem={({item}) => renderList(item)}
ListFooterComponent = {renderFooter}
initialNumToRender={3}
onEndReachedThreshold = {0.1}
refreshing={
<RefreshControl
refreshing = {isLoading}
onRefresh = {onRefresh}
/>
}
onMomentumScrollBegin={() => onEndReachedCalledDuringMomentum = false}
onEndReached = {() => {
if (!onEndReachedCalledDuringMomentum && !isMoreLoading){
getMore();
}
}}
/>
</View>
I referenced the images incorrectly in the firebase. I used the path of the local storage where the images are stored rather than using the given URL of the image.
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
Background
I'm building an app which has at some point a FlatList which renders products. The code for the list looks like this:
<FlatList
data={data}
renderItem={({ item }) => (
<View style={styles.container}>
<View style={styles.left}>
<Text style={styles.title}>{item.name}</Text>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.price}>${item.price}</Text>
<Counter />
</View>
<Image style={styles.right} source={{uri: item.image}}/>
</View>
)}
/>
The data for this list is brought over from a Google Cloud Firestore document. Within this list you can see a component called Counter, its job is to allow the user to add and delete products from their cart. This is its code:
export default function Counter () {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
}
return (
<View style={styles.adder}>
<TouchableOpacity onPress={() => {handleDeletion()}}>
<Text style={styles.less}>-</Text>
</TouchableOpacity>
<Text style={styles.counter}>{count}</Text>
<TouchableOpacity onPress={() => {handleAddition()}}>
<Text style={styles.more}>+</Text>
</TouchableOpacity>
</View>
)
}
Problem
As you can see from the fact that I'm rendering the counter within a FlatList, I need to keep the state stored in the child rather than in the parent, as having the count in the parent would mean that if the user selects one product, every item is added at the same time.
I need to have the a button show up when the user selects a product that allows them to navigate to their purchase summary and also I need that button to display the total cost of their selection and amount of products chosen. As you might imagine, I've no idea how to access the child's state in the parent component.
So to sum it all up:
I have a child with a state update that I need to access from its parent, but I do not know how to do it.
Question¨
Is there any way to listen to event changes in a child's state or passing it up as a prop or something like that?
Thanks a lot in advance!
Extra information
This is image shows the UI of the screen. When pressing the "+" button it updates the count +1 and it should also display a button showing the info I mentioned before.
In renderItem you can pass method callback in here
<Counter onPressFunctionItem={(isPlus) => { // handle from parent here }} />
export default function Counter ({ onPressFunctionItem }) {
const [count, setCount] = useState(0);
const handleAddition=()=>{
setCount(count + 1)
if (onPressFunctionItem) {
onPressFunctionItem(true)
}
}
const handleDeletion=()=>{
{count === 0 ? setCount(count) : setCount(count - 1)}
if (onPressFunctionItem) {
onPressFunctionItem(false)
}
}
return (...)
}
Final Output:
You don't really need to pass the child component's state to the parent to achieve the same result, you can do that very easily the conventional way.
Here is the source code of above example:
export default function App() {
const [products, setProducts] = useState(data);
/*
with this function we increase the quantity of
product of selected id
*/
const addItem = (item) => {
console.log("addItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity + 1,
};
}
return product;
});
setProducts(temp);
};
/*
with this function we decrease the quantity of
product of selected id, also put in the condition so as
to prevent that quantity does not goes below zero
*/
const removeItem = (item) => {
console.log("removeItem");
let temp = products.map((product) => {
if (item.id === product.id) {
return {
...product,
quantity: product.quantity > 0 ? product.quantity - 1 : 0,
};
}
return product;
});
setProducts(temp);
};
/*
this varible holds the list of selected products.
if required, you can use it as a seperate state and use it the
way you want
*/
let selected = products.filter((product) => product.quantity > 0);
/**
* below are two small utility functions,
* they calculate the total itmes and total price of all
* selected items
*/
const totalItems = () => {
return selected.reduce((acc, curr) => acc + curr.quantity, 0);
};
const totalPrice = () => {
let total = 0;
for (let elem of selected) {
total += elem.quantity * elem.price;
}
return total;
};
useEffect(() => {
console.log(products);
}, [products]);
return (
<View style={styles.container}>
<FlatList
data={products}
renderItem={({ item }) => {
return (
<Card style={styles.card}>
<View style={styles.textBox}>
<Text>{item.name}</Text>
<Text>$ {item.price.toString()}</Text>
<View style={{ flexDirection: "row" }}></View>
<View style={styles.buttonBox}>
<Button
onPress={() => removeItem(item)}
title="-"
color="#841584"
/>
<Text>{item.quantity.toString()}</Text>
<Button
onPress={() => addItem(item)}
title="+"
color="#841584"
/>
</View>
</View>
<Image
style={styles.image}
source={{
uri: item.image,
}}
/>
</Card>
);
}}
/>
<View style={{ height: 60 }}></View>
{selected.length && (
<TouchableOpacity style={styles.showCart}>
<View>
<Text style={styles.paragraph}>
{totalItems().toString()} total price ${totalPrice().toString()}
</Text>
</View>
</TouchableOpacity>
)}
</View>
);
}
You can find the working app demo here: Expo Snack