React, render different components and maintain scroll position - reactjs

Suppose you have a horizontal flatlist.
When a user clicks a button in an item, you want to present a view which looks different from the flat-list item you had.
Suppose you implement it like the following
{showDetail ? (
<DetailView onPress={toggleShowDetail} />
) : (
<FlatList
data={data}
renderItem={() => (
<View>
<Button onPress={toggleShowDetail} />{' '}
</View>
)}
/>
)}
Is the scroll position of flatlist maintained when the flatlist is replaced with DetailView and replaced back?
if not, what are the approaches I can take?
I'd like to avoid using modal if possible
edit,
I'm not sure if setting style width=0 would maintain the scroll position when set width=prevSavedWidth .. but definately can try..
import _ from 'lodash'
import React, {useState} from 'react'
import {useDispatch} from 'react-redux'
import {useSelector} from 'react-redux'
import {
Text,
Image,
View,
NativeModules,
NativeEventEmitter,
TouchableOpacity,
FlatList,
} from 'react-native'
const Qnas = props => {
const flatlistRef = React.useRef(null)
const [single, setSingle] = React.useState(false)
let qnas = [
{
title: 'a',
id: 1,
},
{
title: 'b',
id: 2,
},
{
title: 'c',
id: 3,
},
{
title: 'd',
id: 4,
},
{
title: 'e',
},
{
title: 'f',
},
{
title: 'j',
},
]
const toggle = () => {
setSingle(!single)
}
const renderItem = ({item: qna, index}) => {
return (
<View style={{height: 80, width: 200}}>
<Text>{qna.title}</Text>
<TouchableOpacity onPress={toggle}>
<Text>toggle</Text>
</TouchableOpacity>
</View>
)
}
const keyExtractor = (item, index) => {
return `qna-${item.title}-${index}`
}
return (
<View style={{height: 200}}>
{single ? (
<View>
<Text>hello</Text>
<TouchableOpacity onPress={toggle}>
<Text>toggle</Text>
</TouchableOpacity>
</View>
) : (
<FlatList
horizontal
ref={flatlistRef}
data={qnas}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={{
flexDirection: 'column',
flexWrap: 'wrap',
}}
/>
)}
</View>
)
}
export default Qnas

the questions is not clear, but the scroll position of the page is always maintained , even if you render your components on conditional basis. if you want to make sure that you always scroll to the top of the page when you show the Detail view , you can always do window.scrollTo(0, 0 before showDetail flag as true.
Kindly provide more details for better understanding or share a code pen

you can have a handleScroll method which gets called on FlatList Scroll and saves the srollPosition in state
const handleScroll = event => {
console.log(event);
setScrollPosition(event.nativeEvent.contentOffset.x);
};
<FlatList onScroll={handleScroll} />```
and then in your toggle method have a check on if single is true set the scrollPosition of the FlatList using ref
const toggle = () => {
if(single){
this.flatlistRef.scrollToOffset({ animated: true, offset: scrollPosition }
setSingle(!single);
}
hope this helps

Related

How to optimize FlatList in React Native

Can you please tell me how can I optimize this flatlist in react native. I mean how can I do that app will render not the whole list of data but just small part of it for example 10 items, and then when the user will scroll it down it will load more of data from list?
that's the code
import React, {useState, useEffect} from 'react';
import {
SafeAreaView,
StatusBar,
StyleSheet,
Text,
View,
FlatList,
TextInput,
} from 'react-native';
import {newdata} from '../Data/newdata';
const Sample = () => {
const DATA = newdata;
const [searchText, onChangeSearch] = useState('');
const [filteredData, setFilteredData] = useState([]);
useEffect(() => {
const filtered = DATA.filter(item =>
item.title.toLowerCase().includes(searchText.toLowerCase()),
);
if (searchText === '') {
return setFilteredData(DATA);
}
setFilteredData(filtered);
}, [searchText]);
const Item = ({title}) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const renderItem = ({item}) => <Item title={item.title} />;
return (
<SafeAreaView style={styles.container}>
<TextInput
style={{
height: 50,
borderColor: '#919191',
borderWidth: 1,
margin: 10,
paddingLeft: 15,
borderRadius: 10,
}}
onChangeText={newText => onChangeSearch(newText)}
placeholder="Axtaris..."
/>
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={(item, index) => item.key}
/>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
marginBottom: 75,
},
item: {
backgroundColor: '#ededed',
padding: 20,
marginVertical: 2,
marginHorizontal: 10,
borderRadius: 20,
},
title: {
fontSize: 20,
},
});
export default Sample;
P.S. newdata has about 42000 of items, and app running very slow. That is the screenshot of app
You can easily achieve that by using the initialNumToRender prop in FlatList component
How many items to render in the initial batch. This should be enough to fill the screen but not much more. Note these items will never be unmounted as part of the windowed rendering in order to improve perceived performance of scroll-to-top actions.
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={item => item.key}
initialNumToRender={10}
/>
Ali,
You have some features inside Flatlist in order to optimize it for example:
maxtorenderperbatch: This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll.
Read more here
initialNumToRender: Define precise number of items that would cover the screen for every device. [Read more here][1]
Also, you can use Infinite Scroll, is very useful instead of render
the whole list you can render only amount of items, and when the user
is scrolling to the end the app load more items.
onEndReached: Called once when the scroll position gets within onEndReachedThreshold of the rendered content.
onEndReachedThreshold: How far from the end (in units of visible length of the list) the bottom edge of the list must be from the end
of the content to trigger the onEndReached callback.
Here is an example how to use it.
<FlatList
data={filteredData}
renderItem={renderItem}
keyExtractor={(item, index) => item.key}
onEndReached={ loadMoreItems }
onEndReachedThreshold={ 0.5 }
maxToRenderPerBatch={3}
initialNumToRender={5}
/>
loadMoreItems:
const loadMoreItems = ( ) => {
// Here you logic to render more items, when user scroll to the end
}

Adding Placeholder Kind of Functionality into React-Native Picker

I have set up a reusable drop-menu component in my React-native app that uses the native picker. The less than ideal situation I'm running into is that since there is no placeholder property available on picker, if I add 'Select Type' to the list of menu items, that will show up as an option in the menu that opens up. That's obviously not ideal. How can I work a placeholder kind of functionality into this, so that 'Select Type' is initially displayed without showing up in the menu that pops up when the user presses to open the menu?
{
!this.state.otherTypeSelected ?
(
<View style={styles.forms.fieldContainer}>
<Text style={styles.forms.fieldLabel}>Type</Text>
<DropDownMenu
style={styles.forms.pickerContainer}
itemStyle={styles.forms.pickerItem}
items={[
{ label: 'A', value: 'A' },
{ label: 'B', value: 'B' },
{ label: 'C', value: 'C' },
{ label: 'D', value: 'D' },
{ label: 'Other', value: 'Other' }
]}
onSelectMenuValue={this.handleContactTypeSelection}
/>
</View>
) : null
}
And the DropDownMenu code looks like this:
export const DropDownMenu = (props) => {
const [selectedValue, setSelectedValue] = useState(null);
return (
<View style={componentStyles.container}>
<Picker
{...props}
selectedValue={selectedValue}
onValueChange={(itemValue, itemIndex) => {
props.onSelectMenuValue(itemValue),
setSelectedValue(itemValue)
}}
>
{props.items.map(item => <Picker.Item label={item.label} value={item.value} />)}
</Picker>
</View>
);
}
Note, while I could make my first menu item an empty string, with something like { label: '', value: 0 }, the space representing that value will still show up in my drop-menu, which is also not what I'm looking for.
The Picker in react-native has been deprecated. There are only two methods in order to use the Picker
Use any library for Picker.
Create your own Picker (where you will have all the controls with you.)
You can create a simple functional component such as:
Note: This was created in 5 mins, so don't expect this to be pretty :P But it is working
import React, { useState } from "react";
import { Text, View, StyleSheet, TouchableOpacity } from "react-native";
// Dropdown Item component
const DropDownItem = (props) => {
return <TouchableOpacity onPress={() => {
props.onPress(props.label)
props.hideDropdown(false)
}}>
<Text>{props.label}</Text>
</TouchableOpacity>
}
// Custom Picker Component
const CustomPicker = () => {
const [showValue, toggleView] = useState(false)
const [selectedVal, changeValue] = useState('')
return <>
{selectedVal && <TouchableOpacity onPress={() => changeValue('')}>
<Text>Clear Value</Text>
</TouchableOpacity>}
<View style={styles.pickerView}>
<TouchableOpacity style={styles.placeholderView} onPress={() => toggleView(!showValue)}>
<Text style>{selectedVal || "Select a value..."}</Text>
</TouchableOpacity>
{showValue && <View style={styles.dropDownView}>
<DropDownItem label="Value 1" hideDropdown={toggleView} onPress={val => changeValue(val)} />
<DropDownItem label="Value 2" hideDropdown={toggleView} onPress={val => changeValue(val)} />
<DropDownItem label="Value 3" hideDropdown={toggleView} onPress={val => changeValue(val)} />
</View>}
</View>
</>
}
const App = () => {
return (
<View style={styles.container}>
<CustomPicker />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 40,
alignItems: "center"
},
pickerView: {
width: 160,
height: 30,
padding: 3,
},
placeholderView: {
flex: 1,
backgroundColor: '#e0e0e0',
padding: 3
},
dropDownView: {
position: 'absolute',
top: 32
}
});
export default App;
Link: https://snack.expo.io/MBb60jEQw
Happy Coding !!!

React Native: Why does FlatList re-render completely on data changes?

thanks for reading my question! I am struggling with this problem since a few days now: My Flatlist component re-renders all items in the list every time I make a change to the underlying data.
Situation:
I have a FlatList component rendering Items which contain a TouchableOpacity object to toggle Favorite status on this item.
If this button is pressed, I expect only this specific item to change/re-render in my FlatList instead of all items. It feels like as soon as I update state by calling setListData, it re-renders everything.
I have encountered this issue in a more complex setup but was able to drill it down to this core problem. Or is this actually the expected behavior?
Code:
import React, { useState } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
} from "react-native";
const PlanerScreen = () => {
const [listData, setListData] = useState([
{ id: "1", name: "Banana", isFav: true },
{ id: "2", name: "Apple", isFav: false },
]);
const Item = ({ item, onPressHandler }) => {
console.log(item.name, " rendered");
const color = item.isFav ? "red" : "green";
return (
<View
style={{
flexDirection: "row",
width: "100%",
margin: 10,
}}
>
<Text>{item.name}</Text>
<TouchableOpacity
style={{ width: 100, height: 50, backgroundColor: color }}
onPress={onPressHandler}
/>
</View>
);
};
const favHandler = (id) => {
setListData(
listData.map((item) =>
item.id === id ? { ...item, isFav: !item.isFav } : item
)
);
};
console.log("FlatList rendered");
return (
<View style={{ flex: 1 }}>
<StatusBar style={selectedTheme === "light" ? "dark" : "light"} />
<FlatList
data={listData}
renderItem={({ item }) => (
<Item item={item} onPressHandler={() => favHandler(item.id)} />
)}
keyExtractor={(item) => item.id}
/>
</View>
);
};
export default PlanerScreen;
Console Output on clicking the Favorite Toggle Button:
FlatList rendered
Banana rendered
Apple rendered
FlatList rendered
Banana rendered
Apple rendered
FlatList rendered
Banana rendered
Apple rendered
You can use React.memo which is an alternative to shouldComponentUpdate for functional components.
It tells React when to re-render the component based on prev and next props.
import React, { useState, useCallback } from "react";
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
} from "react-native";
const styles = StyleSheet.create({
container: {
flex: 1,
}
})
const keyExtractor = (item) => item.id;
const Item = React.memo(({ item, onPressHandler }) => {
console.log(item.name, " rendered");
const color = item.isFav ? "red" : "green";
return (
<View
style={{
flexDirection: "row",
width: "100%",
margin: 10,
}}
>
<Text>{item.name}</Text>
<TouchableOpacity
style={{ width: 100, height: 50, backgroundColor: color }}
onPress={() => onPressHandler(item.id)}
/>
</View>
);
}, (prevProps, nextProps) => {
if (prevProps.item.isFav === nextProps.item.isFav) return true;
return false;
});
const PlanerScreen = () => {
const [listData, setListData] = useState([
{ id: "1", name: "Banana", isFav: true },
{ id: "2", name: "Apple", isFav: false },
]);
const favHandler = useCallback((id) => {
setListData(prevState => {
return prevState.map((item) =>
item.id === id ? { ...item, isFav: !item.isFav } : item
)
}
);
}, []);
console.log("### FlatList rendered #####");
return (
<View style={styles.container}>
<FlatList
data={listData}
renderItem={({ item }) => <Item item={item} onPressHandler={favHandler} />}
keyExtractor={keyExtractor}
/>
</View>
);
};
export default PlanerScreen;

Select items and transfer selected items to another page

Hello I am creating an app, and am having difficulties trying to create a way where the user selects multiple images, which will then be passed on to another screen. Could I please get some help on this?
Much will be appreciated.
So, the way my app works is that, the user selects multiple items, then there should be an add button or a save button, that will get the selected items and display them to another screen. The items have a value which are Images, not text. This is purely the reason why I asked the question here because most of the React-Native tutorials include values based on text, rather than Images.
The problem I am having, is trying to figure out a way for the user to select multiple items, and clicking a save button, which will in return transfer all of the "selected items" to another screen to be display there. Much like a viewer.
import React, { Component } from 'react';
import { Text, View, StyleSheet, AppRegistry, FlatList, Image, TouchableOpacity } from 'react-native';
import flatListData from '../database';
class FlatListItem extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'FirstScreen!'
})
render() {
return (
<View style={{
flex: 1,
flexDirection:'column',
}}>
<View style={{
flex: 1,
flexDirection:'row',
}}>
<View style={{
flex: 1,
flexDirection:'column',
height: 100
}}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('SecondScreen')} >
<Image source={{uri: this.props.item.imageUrl}}
style={{width: 100, height: 100, margin: 5}}></Image>
</TouchableOpacity>
</View>
</View>
<View style={{
height: 1,
backgroundColor:'white'
}}>
</View>
</View>
);
}
}
class FirstScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'First Screen'
})
render() {
return (
<View style={{flex: 1, marginTop: 22}}>
<FlatList
data={flatListData}
renderItem={({item, index})=>{
//console.log(`Item = ${JSON.stringify(item)}, index = ${index}`);
return (
<FlatListItem item={item} index={index}>
</FlatListItem>);
}}
>
</FlatList>
</View>
);
}
}
export default example;
const styles = StyleSheet.create({
flatListItem: {
color: 'white',
padding: 10,
fontSize: 16,
}
});
Since you did not provide any sample code, so I will try to suggest a way to handle via pseudocode
You can abstract out the list of images into a centralized helper class, then you render from this helper class for user to select.
Now when user have selected one of the image, you just need to capture the ID or any unique identifier, and pass it to second screen.
On this second screen, just using this ID/unique identifier that you've received and search it from the aforementioned centralized helper class and render it.
Looks like you have two things to figure out;
one is keeping track of what items a user has selected on your image selection screen
sending the data back between screens
Looks like you are most likely using react-navigation based on your example so the simplest solution would be to take advantage of React's state and use react-navigation's parameter passing between screens.
With react-navigation you can use the second argument in navigation.navigate to pass parameters/callbacks to the other screen. So you can navigate to a screen and pass a callback to it as such.
...
this.props.navigation.navigate(
'ItemSelectionScreen',
{ onSubmit: (items) => { /* Do something with items */ } }
)
...
And here is a basic example of a selection screen with some comments to explain how it works.
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'grey',
alignItems: 'center',
justifyContent: 'center'
}
});
class ItemSelectionScreen extends React.Component {
constructor(props) {
super(props);
this.onSubmit = () => props.navigation.getParam('onSubmit'); // Keep the passed callback once we have navigated to this screen
this.state = {
selectedItems: [] // Our initial selectedItems (empty)
};
}
handleToggle = (item, isSelected) => {
const { selectedItems } = this.state;
this.setState({ selectedItems: isSelected ? selectedItems.filter(ent => ent !== item) : [...selectedItems, item] }); // Toggle if an item is selected
};
handleSubmitAndExit = () => {
const { onSubmit } = this;
const { selectedItems } = this.state;
onSubmit(selectedItems); // Pass your selectedItems back to the other screen
this.props.navigation.goBack(); // And exit the screen
};
handleExit = () => {
this.props.navigation.goBack(); // Exit the screen without calling onSubmit
};
renderItem = (item, index) => {
const { selectedItems } = this.state;
const isSelected = selectedItems.some(ent => ent === item); // Determine if an item is selected
return (
<TouchableOpacity key={index} onPress={() => this.handleToggle(item, isSelected)}>
<Text>{`${isSelected ? 'X' : 'O'} ${item}`}</Text>
</TouchableOpacity>
);
};
render() {
return (
<View style={styles.container}>
{['item1', 'item2', 'item3'].map(this.renderItem)}
<TouchableOpacity onPress={this.handleSubmitAndExit}>
<Text>Submit and Exit</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleExit}>
<Text>Exit</Text>
</TouchableOpacity>
</View>
);
}
}
export default ItemSelectionScreen;
Good luck and hope this was helpful.

ScrollView RTL in react native

The following code is my ScrollView in a react native project:
<ScrollView
ref={(scrollView) => { this._scrollView = scrollView; }}
horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
directionalLockEnabled={true}
bounces={false}
scrollsToTop={false}
>
Now it moves from left to right, How it could move from right to left in first loading?
For RTL setting you should write your code like the below sample:
import React, { useRef } from 'react';
import { ScrollView, StyleSheet } from 'react-native';
const RTLScrollView = () => {
const scrollRef = useRef();
const scrollToEnd = () => scrollRef.current.scrollToEnd({ animated: false });
return (
<ScrollView
horizontal
ref={scrollRef}
showsHorizontalScrollIndicator={false}
onContentSizeChange={scrollToEnd}
contentContainerStyle={styles.contentContainerStyle}
>
~~~
~~~
~~~
</ScrollView>
);
}
const styles = StyleSheet.create({
contentContainerStyle: {
flexDirection: 'row-reverse'
}
});
export default RTLScrollView;
Hint: I don't use your other ScrollView settings like bounces={false}, If you want, put it in your code, my answer is just a sample.
This is truly an annoying Bug in React Native , ScrollView+RTL=Silly Bug.
Though , there are multiple hacks you can adapt , I did this to overcome the bug :
I reversed the data array I am using.
Used : onContentSizeChange event handler to trigger the
scrollToEnd({ animated: false }) function on ScrollView
you can try invertible-scroll-view that supports horizontal and vertical scroll view
As #SlashArash has mentioned you can use the react-native-invertible-scroll-view.
Here's an example:
import React, { Component } from 'react';
import { View, Text, ScrollView} from 'react-native';
import InvertibleScrollView from 'react-native-invertible-scroll-view';
export default class Demo extends Component {
constructor(props) {
super(props);
this.scrollView = null;
}
render() {
let categories = ['one', 'two'];
categories = categories.map((category, index) => {
return (
<Text>{category}</Text>
)
});
return (
<View style={{
flex: 1,
}}>
<InvertibleScrollView inverted
ref={ref => { this.scrollView = ref }}
onContentSizeChange={() => {
this.scrollView.scrollTo({y: 0, animated: true});
}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
{categories}
</InvertibleScrollView>
</View>
)
}
}
I had to build an autocomplete with an RTL scrolling. This proved to be tricky because the scroll to end solution caused a lot of flickering. I found the best way to make anything RTL is using the transform style. If you use transform, it is important that you apply the transform to each item in the list as well. Otherwise, you will have mirrored text. This solution also works if you want to invert top/bottom, just change transform scaleY instead of x.
Here is my ScrollView:
const renderList = () => {
return itemList.map(item => (
<Item
key={item.id}
address={item}
style={{ transform: [{ scaleX: -1 }] }}
/>
));
};
....
<ScrollView
contentContainerStyle={styles.scrollViewContentContainer}
horizontal
keyboardShouldPersistTaps="always"
ref={scrollView}
style={styles.scrollView}
>
{renderList()}
</ScrollView
Here is the corresponding ScrollView style:
const styles = StyleSheet.create({
scrollView: {
marginRight: 10,
transform: [{ scaleX: -1 }]
},
scrollViewContentContainer: {
flexGrow: 1,
justifyContent: 'flex-end',
}
});

Resources