React Navigation only sets param after navigating a second time - javascript

I want to pass the item (asset) from a Flastlist and present in a child screen.
But when I press the item, the parameter is null. I have to go back and press it again for the parameter to be set. And if I press a different item, the old item still lingers until I press the new item a second time.
I don't know if useEffect is the best way to do it. I am just trying different approaches but have not had any luck with useEffect, useFocusEffect, or none.
Parent with the Flastlist
export default function SitesScreen(props) {
const [sites, setSites] = useState(["Power Plant", "Paper Mill", "Plastic Injection"])
const [selectedItem, setSelectedItem] = useState(null)
const Item = ({ item, onPress }) => (
<TouchableOpacity onPress={onPress} style={[styles.item]} >
<Text style={styles.text}>{item}</Text>
</TouchableOpacity>
)
const renderItem = ({ item }) => {
return (
<View style={styles.itemContainer} >
<Item
item={item}
onPress={() => onItemSelected(item)}
/>
</View >
)
}
const onItemSelected = (item) => {
setSelectedItem(item)
props.navigation.navigate("Asset", { asset: selectedItem })
}
return (
<View style={styles.container}>
<CustomHeader title="Sites" navigation={props.navigation} isHome={true} ></CustomHeader>
<View style={styles.contentContainer}>
<View style={{ width: '90%', height: '50%', alignItems: 'center', bottom: -150 }} >
<FlatList
data={sites}
renderItem={renderItem}
keyExtractor={(item) => JSON.stringify(item)}
/>
</View>
</View>
</View>
)}
Child screen to present item
export default function SitesScreen(props) {
const [asset, setAsset] = useState('')
useEffect(() => {
setAsset(props.route.params.asset)
console.log(asset)
}, [])
return (
<View style={styles.container}>
<CustomHeader title="Asset" navigation={props.navigation} isHome={false} ></CustomHeader>
<View style={styles.contentContainer}>
<Text style={styles.text} >{asset}</Text>
<View style={{ width: '90%', height: '50%', alignItems: 'center', bottom: -150 }} >
</View>
</View>
</View>
)}

When you pass the selectedItem value as a parameter to the next screen, the new state set from setSelectedItem has not been applied to the component yet. Because the new state hasn't been applied, you are still passing the initial null value that was set for selectedItem. This happens because state values are used by functions based on their current closures.
See this StackOverflow post for a more detailed explanation about this problem.

Problem solved.
The parent screen needs to have the navigate action in the useEffect hook, and not in the onItemSelected function. That way it waits until the state has been changed before it navigates.
useEffect(() => {
if (selectedItem) {
props.navigation.navigate("Asset", { asset: selectedItem })
}
}, [selectedItem])

Related

ScrollView Pagination causes all children to re-render

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.

react native flatlist doesn't show updated value

I'm having a trouble in a flatlist that a button increments the value but doesn't show if I don't refresh or hit ctrl-s after incrementing value. Basically I cannot see the value change without pressing ctrl-s or going to another page and returning to the same page.
<FlatList
data={store}
renderItem={({ item }) => {
return (
<View style={styles.itemCountView}>
<TouchableOpacity style={styles.up}
onPress={() => item.itemCount++}>
<MaterialIcons name="arrow-drop-up" size={36} color="#ddd"/>
</TouchableOpacity>
<Text style={styles.itemCountText}>{item.itemCount}</Text>
</View>
)
}}
/>
I can increment the value, if I save or go to another page and come back the value changes and i can see it but there must be a way to see it change.
Any help is appreciated
As per the Documentation
extraData: https://reactnative.dev/docs/flatlist#extradata
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
I think you'r not saving the changes you doing, basically you need to update the the store variable each time you increment an item, something like:
<FlatList
data={store}
renderItem={({ item }) => {
return (
<View style={styles.itemCountView}>
<TouchableOpacity style={styles.up}
onPress={() => {
const newStoreData = [...store]
const itemIndex = newStoreData.findIndex(item)
const newItem = {...item,itemCount:item.itemCount ++}
newStoreData[itemIndex] = newItem
setStoreData(newStoreData) // state or redux?
}
}>
<MaterialIcons name="arrow-drop-up" size={36} color="#ddd"/>
</TouchableOpacity>
<Text style={styles.itemCountText}>{item.itemCount}</Text>
</View>
)
}}
/>
You can try this approach here:
import {useState} from 'react';
import { Text, View, FlatList, TouchableOpacity } from 'react-native';
const store = [{
itemCount: 1
},
{
itemCount: 2
},
{
itemCount: 3
}]
export default function App() {
return (
<View style={{
flex: 1,
padding: 140,
}}>
<FlatList
data={store}
renderItem={({ item }) => ( <Item item={item}/> )}
/>
</View>
);
}
const Item = ({item})=> {
const[count, setCount] = useState(item.itemCount);
return (
<View>
<TouchableOpacity
style={{
backgroundColor: 'black',
padding: 10,
margin: 10
}}
onPress={() => setCount(count+1)}>
<Text style={{
color: 'white'
}}>{count}</Text>
</TouchableOpacity>
</View>
)
}
This will set the state for each item in the Flatlist.

How to make a dropdown for every FlatList item?

Kind of surprised at the lack of information I can find about this question, feel like its something that you be done pretty often? I know with a standard dropdown it's pretty easy, you set up a hook that controls state with a boolean, when that boolean is true, you show the dropdown, when it's false, you show the closed version.
The Issue that I discovered when trying to do this with each render item is that the hook state control needs to be in the global context, because of this whenever you click on Flatlist item, all of the items open because they are all using the same state control. How would you make it so that each rendered item has its own dropdown state when you can't set a state hook inside each render item?
Here's my code to give you a better idea of what I'm talking about, be sure to read the comments:
<FlatList
contentContainerStyle={{ alignItems: 'center', marginVertical: 10, minHeight: 200 }}
data={notes}
keyExtractor={(item, index) => item.key}
ListFooterComponent={() => <AddNoteFooter onPress={addSpecificNote} />}
renderItem={({ item }) => {
//would need something similar to a hook right here,
// to manage the state of each item individually
//change "isOpen" state when button is pressed
return (
<View>
{!isOpen &&
<TouchableOpacity onPress={null} style={styles.flastliststyle}>
<Text style={styles.flastlistItemText}>{item.note}</Text>
</TouchableOpacity>
}
{isOpen &&
<TouchableOpacity>
/* extended dropdown code */
</TouchableOpacity>
}
</View>)
}
Looking to do something similar to this video but where each item is a flatlist item (also using hooks):
https://www.youtube.com/watch?v=awEP-pM0nYw&t=134s
Thank you!
SOLUTION:
flatlist:
<FlatList
contentContainerStyle={{ alignItems: 'center', marginVertical: 10, minHeight: 200 }}
data={notes}
keyExtractor={(item, index) => item.key}
ListFooterComponent={() => <AddNoteFooter onPress={addSpecificNote} />}
renderItem={({ item }) => {
return (
<NoteItem noteitem={item} />
)
}}
/>
then the component rendered for each item:
const NoteItem = (props) => {
const [isOpen, updateDrop] = useState(false)
return (
<View>
{!isOpen &&
<TouchableOpacity onPress={() => updateDrop(prev => !prev)} style={styles.flastliststyle}>
<Text style={styles.flastlistItemText}>{props.noteitem.note}</Text>
</TouchableOpacity>
}
{isOpen &&
<TouchableOpacity onPress={() => updateDrop(prev => !prev)} style={styles.flastliststyle}>
<Text style={styles.flastlistItemText}>pressed</Text>
</TouchableOpacity>
}
</View>
)
}

React Native Swiper ref element scrollTo does not work on first render

I am implementing an Introduction feature in my app. Where the screens show after splash screen. I used react-native-swiper component for guiding the user to step by step tutorial in my app.
Here is my Slider.tsx component code below.
type SliderProps = {
// swiperEl: RefObject<Swiper>;
// onIndexChanged: (index: number) => void;
index: number;
};
const Slider = ({ index }: SliderProps) => {
const swiperEl = useRef(null);
useEffect(() => {
swiperEl.current!.scrollTo(index);
}, [index, swiperEl]);
return (
<View style={styles.sliderContainer}>
<Swiper
scrollEnabled={false}
index={index}
ref={swiperEl}
style={styles.wrapper}
>
<View style={styles.slide1}>
<View style={[styles.circle, { backgroundColor: '#FF7F50' }]} />
</View>
<View style={styles.slide2}>
<View style={[styles.circle, { backgroundColor: '#FF6347' }]} />
</View>
<View style={styles.slide3}>
<View style={[styles.circle, { backgroundColor: '#FF4500' }]} />
</View>
</Swiper>
</View>
);
};
and here is my WelcomeScreenContainer.tsx
const WelcomeScreen = () => {
const [currentPage, setPage] = useState(0);
const navigation = useNavigation();
const handleNextClick = () => {
if (currentPage === 2) {
navigation.navigate('LoginScreen');
return;
}
setPage((prevPage) => prevPage + 1);
};
const handleSkipPress = () => navigation.navigate('LoginScreen');
console.log('Parent', currentPage);
return (
<View style={styles.parentContainer}>
<View style={styles.headerContainer}>
{/* <Text style={{ fontSize: 35 }}>WelcomeScreen</Text> */}
</View>
<Slider index={currentPage} />
<View style={{ flex: 1 }} />
<ButtonContainer
onNextPress={handleNextClick}
onSkipPress={handleSkipPress}
/>
</View>
);
};
Whenever I click the next button it should automatically slide the swiper to the next component or screen. However, on the first render when the user clicks the next button the swiperEl.current!.scrollTo(index); on the useEffect block of Slider.tsx does not work. But when the user clicks for the second time not it suddenly works.
I am a beginner in using React but I follow the documentation carefully on how to use the hooks of useRef and useEffect. Maybe I am missing something?
Appreciate it if someone could help.
Thanks
https://www.npmjs.com/package/react-native-swiper now supports scrollTo feature.
I havent seen this in official documentation but maybe i wasnt careful enough, Here is what worked for me in year 2022
...
const milestoneData = [1, 2, 3, 4, 5] //your data
const mileStoneSwiperRef = useRef(null);
const scrollMileStoneTo = (index) => {
mileStoneSwiperRef?.current?.scrollTo(index);
}
return (<View>
<View style={{padding: 10, backgroundColor: BaseColor.mainColor2}}>
<ScrollView showsHorizontalScrollIndicator={false} alwaysBounceHorizontal={true} horizontal={true} >
{milestoneData?.map((item, index) => <>
<Pressable onPress={() => scrollMileStoneTo(index)} style={{padding: 5, marginRight: 10, alignItems: 'center'}}>
<Image source={Images.math} style={[styles.contact, {width: 60, height: 60}]}/>
</Pressable></>)}
</ScrollView>
</View>
<Swiper
ref={mileStoneSwiperRef}
loop={false}
centeredSlides={false}
showsPagination={false}
bounces={true}
removeClippedSubviews={false}
>
{milestoneData?.map((item, index) => <View>
<View style={styles.groupHeadingViews}>
<Text fontResizeable bold style={styles.escrowGroupHeading}>{(`${tradeMethod} Name`).toUpperCase()}</Text>
<View style={styles.subTitleAndWordCount}>
<Text fontResizeable style={styles.escrowGroupSubHeading}>Briefly Name Your Country</Text>
<Text fontResizeable style={styles.escrowGroupSubHeading}>{milestoneData?.[index]?.name?.length}/30</Text>
</View>
</View>
</View>)}
</Swiper>
</View>)
...
The trick to get the scrollTo() an index done is the following method
const scrollMileStoneTo = (index) => {
mileStoneSwiperRef?.current?.scrollTo(index);
}

border bottom changing on press flatlist item in react native

i'm new in react native and i need your help
i want to give border-bottom to pressed item of horizontal flatlist and border-bottom of Previous item disappear,
now i can give border to new pressed item but i cant remove previous item border
how can i achive this?
enter image description here
these are my Category_style code
state = {
isModalVisible: false,
Index : 0
}
_toggleModal = (index) => {
this.setState({isModalVisible: !this.state.isModalVisible});
this.setState({Index : index});
}
renderProduct(item) {
return <Sub_Categories_FlatList_style name={item.title} icon={item.icon}/>
}
renderSeparator = () => (
<View
style={{
backgroundColor: '#d2d2d2',
height: 0.5,
}}
/>
)
render() {
const {title, index} = this.props;
return (
<View style={pStyles.container}>
<TouchableHighlight onPress={() => this._toggleModal(index)}
style={(index === this.state.Index) ? pStyles.border_bottom : pStyles.no_border_bottom}>
<Text style={pStyles.title}>{title}</Text>
</TouchableHighlight>
<Modal isVisible={this.state.isModalVisible} animationType={'slide'}>
<TouchableOpacity onPress={() => this._toggleModal(index)} style={pStyles.T_opacity}
activeOpacity={.7}>
<Image source={require('./../pictures/x_icon.png')}
style={pStyles.close_image}/>
</TouchableOpacity>
<View style={pStyles.in_modal_view}>
<Text style={pStyles.modal_header_text}>{title}</Text>
<FlatList
data={this.props.subCategory}
renderItem={({item}) => this.renderProduct(item)}
ItemSeparatorComponent={this.renderSeparator}
keyExtractor={(item, index) => index}/>
</View>
</Modal>
</View>
and these are my Category code
static navigationOptions = {
headerStyle: {
backgroundColor: '#4caf50'
},
headerTitle: <Text style={Category_in_style.headerTitleStyle}>Category</Text>
}
renderCategory(item, index) {
return <Category_style title={item.title} index={index} subCategory={item.sub_category}/>
}
renderProduct(item) {
return <Product_style image={item.imageUrl} title={item.title} price={item.price}/>
}
render() {
return (
<View style={{backgroundColor: 'white'}}>
<FlatList style={styles.first_flat}
horizontal
data={this.state.main_categories}
renderItem={({item, index})=> this.renderCategory(item, index)}
keyExtractor={(item, index) => index}/>
<PTRView style={{backgroundColor: '#f1f1f1'}}>
<FlatList style={[{marginTop: 10}, {marginBottom: 50}]}
data={this.state.articles}
renderItem={({item}) => this.renderProduct(item)}
keyExtractor={(item, index) => index}
numColumns={2}/>
</PTRView>
</View>
There may be multiple ways of doing this but I prefer having the parent decide which component is clicked.
So what I suggest is in your renderProduct the item object also has an index which you can pass to your Sub_Categories_FlatList_style and also pass a function to it which basically updates a variable whenever the item is clicked.
Then when you render the list item simply check if it's index matches the currently selected index and then style it accordingly.
Sorry if this seems a bit vague but I'm eyeballing this since I'm at work but I'll be more than happy to answer any follow ups.

Categories

Resources