I am passing a ref to a FlatList and expecting to access the scrollToIndex() function, but it doesn't appear in the console and it throws an error when I try to use it saying "scrollTo() is not a function", it doesn't even recognise that I'm trying to use scrollToIndex().
This is a simple example of usage in my code:
const ref = useRef()
ref.current?.scrollToIndex({index:itemIndex,animated:true})
<FlatList ref={ref} .../>
Has anyone experienced this and knows how to make it work? I've looked over the documentation and examples and everything seems to be fine. Any help would be appreciated!
UPDATE:
Component:
import React from "react";
import { View, FlatList, ImageBackground,TouchableOpacity,ScrollView,StyleSheet, Text} from "react-native";
const Projects = ({ index, navigation }) => {
const {width} = Dimensions.get("window")
const [imageIndex, setIndex] = React.useState(0)
const ref = React.useRef()
const wItem = (width - 63) / 2;
console.log("index projects", imageIndex)
const scrollToIndex = (index)=>{
if(ref.current){
ref.current.scrollToIndex({index:index})
}
}
const goToIndex = React.useCallback((info) => {
const wait = new Promise(resolve => setTimeout(resolve, 200));
wait.then(() => {
ref.current?.scrollToIndex({ index: info.index, animated: true });
})
}, [])
return (
<ScrollView >
<TouchableOpacity onPress={()=> scrollToIndex(5)}><Text>Scroll to</Text></TouchableOpacity>
<FlatList ref={ref}
nestedScrollEnabled
numColumns={2}
data={DATA}
onScrollToIndexFailed={(index)=>goToIndex(index)}
style={styles.flatList}
keyExtractor={(i, index) => index.toString()}
renderItem={({ item, index }) => (
<View style={styles.item}>
<ImageBackground
source={item.image}
/* #ts-ignore */
imageStyle={styles.imgStyle}
style={{ width: wItem, height: wItem, alignItems: "flex-end" }}>
</ImageBackground>
</TouchableOpacity>
<Text>{item.title}</Text>
</View>
)}
/>
</ScrollView>
);
};
export default Projects;
const styles = StyleSheet.create({
container: {
flex: 1,
},
item: {
marginRight: 15,
marginBottom: 24,
alignItems:'center'
},
flatList: {
marginHorizontal: 24,
marginTop: 16,
},
imgStyle: {
borderRadius: 12,
},
});
const DATA = [//list of local images]
It is unable to call scrollTo because the ref has only been initialized and not assigned at the time of use:
const ref = useRef()
// ref = null
ref.current?.scrollToIndex({index:itemIndex,animated:true})
// ref = null
<FlatList ref={ref} .../>
// ref != null
So ref is not assigned until you render for the first time. This means that if you want to call .scrollTo() on the ref you can do it in 2 different ways:
1 - If you want to scroll to a certain index initially:
useEffect(() => {
const initialIndex = 34 //random number
if (ref.current){
ref.current.scrollToIndex({index: initialIndex})
}
}, [ref])
The ref in the dependency array will make sure that the useEffect will run whenever the value of ref changes (in this case it will change once it is assigned to a value by rendering the <Flatlist />
2 - If you want to scroll to a certain index on demand:
const handleScrollTo = (index) => {
if (ref.current && typeof ref.current.scrollToIndex === 'function'){
ref.current.scrollToIndex({index: index})
}
}
This function should be connected to something like an onPress or similar, which means the ref should be defined whenever the user presses the button because being able to see the button and press it requires that the component has rendered. Even though ref.current should already be assigned once it is used here, it is always good practice to make sure that it doesn't crash in cases where it is not, which is why we are adding the if-statement.
Related
How can I use the refText to update the element 'Text'
const refText = null;
const doSomething = () =>{
refText.changeVisibility("hidden"); // something like that
}
return <Text ref={refText} onPress={doSomething}>Some Text</Text>;
I tried to find any way to work with it, but can't find any solution over google. Or maybe I missed.
setNativeProps may be what you're looking for, however this is generally not the recommended way to make updates to an element. Instead, consider using state and updating relevant props/styles of your component in response to that state change:
Example on Expo Snack
import { useState } from 'react';
import { Text, View, StyleSheet, Button } from 'react-native';
export default function App() {
const [visible, setVisible] = useState(true);
return (
<View style={styles.container}>
<Button title="Toggle" onPress={() => setVisible(previous => !previous)} />
{visible && (
<Text onPress={() => setVisible(false)} style={{padding: 20}}>Click this text to hide it</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
I have a simple TextInput and a submit button and I want to handle text change without re render the componenent. On a class component I use a private variable "textContent" triggering OnChangeText event instead of a state to prevent forced rendering but I don't know how to do that with function component. Is it possible to store the text value without state on function component ? Thanks
Here is the component (that re render on text change) :
const Search = () => {
const [_films, setFilms] = useState([]);
const [_text, setText] = useState('');
const _loadFilms = () => {
console.log(_text);
if (_text.length > 0) {
getFilmsFromApiWithSearchedText(_text).then(data => {
setFilms(data.results);
});
}
}
console.log("rendering");
return (
<View style={styles.main_container}>
<TextInput style={styles.textinput} placeholder='Titre du film' onChangeText={(text) => setText(text)} />
<Button title='Rechercher' onPress={() => { _loadFilms() }} />
<FlatList
data={_films}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <FilmItem film={item} />}
/>
</View>
);
}
const styles = StyleSheet.create({
main_container: {
flex: 1,
marginTop: 20
},
textinput: {
marginLeft: 5,
marginRight: 5,
height: 50,
borderColor: '#000000',
borderWidth: 1,
paddingLeft: 5
}
});
export default Search;`
On a class component you could store the text in a private variable of the class. With functional components you can use refs for something equivalent. More info about how, available in this SO question
I have 2 screens in my App one that has a form where the user stores the data that it fills in AsyncStorage and this screen that reads all the data saved in AsyncStorage and should show the data in a FlatList. The problem here is that nothing is rendered and the screen is blank. I dont know where the problem is located because if you read the code the console.log(productosData) actually returns in my command line exactly the same structure of result. So productosData is loaded without problem but for some reason this doesn't work.
export default function TestScreen () {
const [productosData, setproductosData] = useState([]);
const ItemView = ({item}) => {
return (
<View>
<Text>
{item}
</Text>
</View>
);
};
useEffect( () => {
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
result.forEach(element => (setproductosData(productosData.push(element))));
console.log(productosData);
}
cargarEnEstado()
},[])
return (
<View style={styles.container}>
<FlatList
data={productosData}
renderItem={ItemView}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 2,
justifyContent: 'center',
alignItems: 'center',
}
});
So I maybe thought that the problem was my FlatList and then I decided to take my FlatList out and test the hook with a Text but when I use {productosData} inside the Text the screen shows a number that corresponds with the length of the first array of result. So in this case I see in the screen a 4 because result as well as productosData have this structure [["a","b"],["c","d"],["e","f"],["g","h"]] with the length of the first array being 4.
export default function TestScreen () {
const [productosData, setproductosData] = useState([]);
const ItemView = ({item}) => {
return (
<View>
<Text>
{item}
</Text>
</View>
);
};
useEffect( () => {
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
result.forEach(element => (setproductosData(productosData.push(element))));
console.log(productosData);
}
cargarEnEstado()
},[])
return (
<View style={styles.container}>
<Text> {productosData} </Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 2,
justifyContent: 'center',
alignItems: 'center',
}
});
Any suggestions? Thank you in advance.
The reason nothing is being rendered is because element is an array of strings ['a','b'], if I understood correctly. Try to change your itemView to this
const ItemView = ({item}) => {
return (
<View>
<Text>
{item[0]+ ' , ' + item[1]}
</Text>
</View>
);
};
Also, your useEffect is not very clean. Note that state in React is immutable. By calling push on productosData, you're mutating the state. First, create a shallow copy of your state, then push your new objects into the copy. Only then you would update your state.
However, there is no reason to iterate your results, just spread them on the state, like this:
async function cargarEnEstado() {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
//result = [["a","b"],["c","d"],["e","f"],["g","h"]]
setProductosData([...productosData, ...result])
console.log(productosData); // Also state is async, so this might not log the correct value
}
onViewableItemsChanged does not seem to work when there is a state change in the app. Is this correct?
Seems like it wouldn't be very useful if this were the case....
Otherwise, users will be forced to us onScroll in order to determine position or something similar...
Steps to Reproduce
Please refer to snack
Repo has also been uploaded at github
Any state change produces an error when using onViewableItemsChanged
What does this error even mean?
Note: Placing the onViewableItemsChanged function in a const outside the render method also does not assist...
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={(info) =>console.log(info)}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
Actual Behavior
Error
Based on #woodpav comment. Using functional components and Hooks.
Assign both viewabilityConfig to a ref and onViewableItemsChanged to a useCallback to ensure the identities are stable and use those. Something like below:
const onViewCallBack = React.useCallback((viewableItems)=> {
console.log(viewableItems)
// Use viewable items in state or as intended
}, []) // any dependencies that require the function to be "redeclared"
const viewConfigRef = React.useRef({ viewAreaCoveragePercentThreshold: 50 })
<FlatList
horizontal={true}
onViewableItemsChanged={onViewCallBack}
data={Object.keys(cards)}
keyExtractor={(_, index) => index.toString()}
viewabilityConfig={viewConfigRef.current}
renderItem={({ item, index }) => { ... }}
/>
The error "Changing onViewableItemsChanged on the fly is not supported" occurs because when you update the state, you are creating a new onViewableItemsChanged function reference, so you are changing it on the fly.
While the other answer may solve the issue with useRef, it is not the correct hook in this case. You should be using useCallback to return a memoized callback and useState to get the current state without needing to create a new reference to the function.
Here is an example that save all viewed items index on state:
const MyComp = () => {
const [cardData] = useState(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']);
const [viewedItems, setViewedItems] = useState([]);
const handleVieweableItemsChanged = useCallback(({ changed }) => {
setViewedItems(oldViewedItems => {
// We can have access to the current state without adding it
// to the useCallback dependencies
let newViewedItems = null;
changed.forEach(({ index, isViewable }) => {
if (index != null && isViewable && !oldViewedItems.includes(index)) {
if (newViewedItems == null) {
newViewedItems = [...oldViewedItems];
}
newViewedItems.push(index);
}
});
// If the items didn't change, we return the old items so
// an unnecessary re-render is avoided.
return newViewedItems == null ? oldViewedItems : newViewedItems;
});
// Since it has no dependencies, this function is created only once
}, []);
function renderItem({ index, item }) {
const viewed = '' + viewedItems.includes(index);
return (
<View>
<Text>Data: {item}, Viewed: {viewed}</Text>
</View>
);
}
return (
<FlatList
data={cardData}
onViewableItemsChanged={handleVieweableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
renderItem={renderItem}
/>
);
}
You can see it working on Snack.
You must pass in a function to onViewableItemsChanged that is bound in the constructor of the component and you must set viewabilityConfig as a constant outside of the Flatlist.
Example:
class YourComponent extends Component {
constructor() {
super()
this.onViewableItemsChanged.bind(this)
}
onViewableItemsChanged({viewableItems, changed}) {
console.log('viewableItems', viewableItems)
console.log('changed', changed)
}
viewabilityConfig = {viewAreaCoveragePercentThreshold: 50}
render() {
return(
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>}
/>
)
}
}
In 2023 with react-native version 0.71.2, the following code seems to work better than the older answers.
// 1. Define a function outside the component:
const onViewableItemsChanged = (info) => {
console.log(info);
};
// 2. create a reference to the function (above)
const viewabilityConfigCallbackPairs = useRef([
{ onViewableItemsChanged },
]);
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 50}}
// remove the following statement
// onViewableItemsChanged={(info) =>console.log(info)}
// 3. add the following statement, instead of the one above
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
Source: https://github.com/facebook/react-native/issues/30171#issuecomment-820833606
const handleItemChange = useCallback( ({viewableItems}) => {
console.log('here are the chaneges', viewableItems);
if(viewableItems.length>=1)
viewableItems[0].isViewable?
setChange(viewableItems[0].index):null;
},[])
try this one it work for me
Setting both onViewableItemsChanged and viewabilityConfig outside the flatlist solved my problem.
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
if (viewableItems.length >= 1) {
if (viewableItems[0].isViewable) {
setItem(items[viewableItems[0].index]);
setActiveIndex(viewableItems[0].index);
}
}
}, []);
const viewabilityConfig = {
viewAreaCoveragePercentThreshold: 50,
};
I'm using functional component and my flatlist looks like this
<Animated.FlatList
data={items}
keyExtractor={item => item.key}
horizontal
initialScrollIndex={activeIndex}
pagingEnabled
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={viewabilityConfig}
ref={flatlistRef}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: false },
)}
contentContainerStyle={{
paddingBottom: 10,
}}
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => {
return (
<View style={{ width, alignItems: 'center' }}>
<SomeComponent item={item} />
</View>
);
}}
/>
Try using viewabilityConfigCallbackPairs instead of onViewableItemsChanged.
import React, {useRef} from 'react';
const App = () => {
// The name of the function must be onViewableItemsChanged.
const onViewableItemsChanged = ({viewableItems}) => {
console.log(viewableItems);
// Your code here.
};
const viewabilityConfigCallbackPairs = useRef([{onViewableItemsChanged}]);
return (
<View style={styles.root}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}
/>
</View>
);
}
Move the viewabilityConfig object to the constructor.
constructor() {
this.viewabilityConfig = {
viewAreaCoveragePercentThreshold: 50
};
}
render() {
return(
<FlatList
data={this.state.cardData}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onViewableItemsChanged={(info) =>console.log(info)}
viewabilityConfig={this.viewabilityConfig}
renderItem={({item}) =>
<View style={{width: width, borderColor: 'white', borderWidth: 20,}}>
<Text>Dogs and Cats</Text>
</View>
}
/>
)
}
Sombody suggest to use extraData property of Flatlist to let Flatlist notice, that something changed.
But this didn't work for me, here is what work for me:
Use key={this.state.orientation} while orientation e.g is "portrait" or "landscape"... it can be everything you want, but it had to change, if the orientation changed.
If Flatlist notice that the key-property is changed, it rerenders.
works for react-native 0.56
this works for me, is there any way to pass an additional argument to onViewRef? Like in the below code how can i pass type argument to onViewRef.
Code:
function getScrollItems(items, isPendingList, type) {
return (
<FlatList
data={items}
style={{width: wp("100%"), paddingLeft: wp("4%"), paddingRight: wp("10%")}}
horizontal={true}
keyExtractor={(item, index) => index.toString()}
showsHorizontalScrollIndicator={false}
renderItem={({item, index}) => renderScrollItem(item, index, isPendingList, type)}
viewabilityConfig={viewConfigRef.current}
onViewableItemsChanged={onViewRef.current}
/>
)
}
Remove your viewabilityConfig prop to a const value outside the render functions as well as your onViewableItemsChanged function
I put together a simple React-native application to gets data from a remote service, loads it in a FlatList. When a user taps on an item, it should be highlighted and selection should be retained. I am sure such a trivial operation should not be difficult. I am not sure what I am missing.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
Image,
TouchableOpacity,
} from 'react-native';
export default class BasicFlatList extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: [],
page: 1,
seed: 1,
error: null,
refreshing: false,
selectedItem:'null',
};
}
componentDidMount() {
this.makeRemoteRequest();
}
makeRemoteRequest = () => {
const {page, seed} = this.state;
const url = `https://randomuser.me/api/?seed=${seed}&page=${page}&results=20`;
this.setState({loading: true});
fetch(url)
.then(res => res.json())
.then(res => {
this.setState({
data: page === 1 ? res.results : [...this.state.data, ...res.results],
error: res.error || null,
loading: false,
refreshing: false
});
})
.catch(error => {
this.setState({error, loading: false});
});
};
onPressAction = (rowItem) => {
console.log('ListItem was selected');
console.dir(rowItem);
this.setState({
selectedItem: rowItem.id.value
});
}
renderRow = (item) => {
const isSelectedUser = this.state.selectedItem === item.id.value;
console.log(`Rendered item - ${item.id.value} for ${isSelectedUser}`);
const viewStyle = isSelectedUser ? styles.selectedButton : styles.normalButton;
return(
<TouchableOpacity style={viewStyle} onPress={() => this.onPressAction(item)} underlayColor='#dddddd'>
<View style={styles.listItemContainer}>
<View>
<Image source={{ uri: item.picture.large}} style={styles.photo} />
</View>
<View style={{flexDirection: 'column'}}>
<View style={{flexDirection: 'row', alignItems: 'flex-start',}}>
{isSelectedUser ?
<Text style={styles.selectedText}>{item.name.first} {item.name.last}</Text>
: <Text style={styles.text}>{item.name.first} {item.name.last}</Text>
}
</View>
<View style={{flexDirection: 'row', alignItems: 'flex-start',}}>
<Text style={styles.text}>{item.email}</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
}
render() {
return(
<FlatList style={styles.container}
data={this.state.data}
renderItem={({ item }) => (
this.renderRow(item)
)}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 50,
},
selectedButton: {
backgroundColor: 'lightgray',
},
normalButton: {
backgroundColor: 'white',
},
listItemContainer: {
flex: 1,
padding: 12,
flexDirection: 'row',
alignItems: 'flex-start',
},
text: {
marginLeft: 12,
fontSize: 16,
},
selectedText: {
marginLeft: 12,
fontSize: 20,
},
photo: {
height: 40,
width: 40,
borderRadius: 20,
},
});
When user taps on an item in the list, "onPress" method is invoked with the information on selected item. But the next step of highlight item in Flatlist does not happen. 'UnderlayColor' is of no help either.
Any help/advice will be much appreciated.
You can do something like:
For the renderItem, use something like a TouchableOpacity with an onPress event passing the index or id of the renderedItem;
Function to add the selected item to a state:
handleSelection = (id) => {
var selectedId = this.state.selectedId
if(selectedId === id)
this.setState({selectedItem: null})
else
this.setState({selectedItem: id})
}
handleSelectionMultiple = (id) => {
var selectedIds = [...this.state.selectedIds] // clone state
if(selectedIds.includes(id))
selectedIds = selectedIds.filter(_id => _id !== id)
else
selectedIds.push(id)
this.setState({selectedIds})
}
FlatList:
<FlatList
data={data}
extraData={
this.state.selectedId // for single item
this.state.selectedIds // for multiple items
}
renderItem={(item) =>
<TouchableOpacity
// for single item
onPress={() => this.handleSelection(item.id)}
style={item.id === this.state.selectedId ? styles.selected : null}
// for multiple items
onPress={() => this.handleSelectionMultiple(item.id)}
style={this.state.selectedIds.includes(item.id) ? styles.selected : null}
>
<Text>{item.name}</Text>
</TouchableOpacity>
}
/>
Make a style for the selected item and that's it!
In place of this.state.selectedItem and setting with/checking for a rowItem.id.value, I would recommend using a Map object with key:value pairs as shown in the RN FlatList docs example: https://facebook.github.io/react-native/docs/flatlist.html. Take a look at the js Map docs as well: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map.
The extraData prop recommended by #j.I-V will ensure re-rendering occurs when this.state.selected changes on selection.
Your onPressAction will obviously change a bit from example below depending on if you want to limit the number of selections at any given time or not allow user to toggle selection, etc.
Additionally, though not necessary by any means, I like to use another class or pure component for the renderItem component; ends up looking something like the following:
export default class BasicFlatList extends Component {
state = {
otherStateStuff: ...,
selected: (new Map(): Map<string, boolean>) //iterable object with string:boolean key:value pairs
}
onPressAction = (key: string) => {
this.setState((state) => {
//create new Map object, maintaining state immutability
const selected = new Map(state.selected);
//remove key if selected, add key if not selected
this.state.selected.has(key) ? selected.delete(key) : selected.set(key, !selected.get(key));
return {selected};
});
}
renderRow = (item) => {
return (
<RowItem
{...otherProps}
item={item}
onPressItem={this.onPressAction}
selected={!!this.state.selected.get(item.key)} />
);
}
render() {
return(
<FlatList style={styles.container}
data={this.state.data}
renderItem={({ item }) => (
this.renderRow(item)
)}
extraData={this.state}
/>
);
}
}
class RowItem extends Component {
render(){
//render styles and components conditionally using this.props.selected ? _ : _
return (
<TouchableOpacity onPress={this.props.onPressItem}>
...
</TouchableOpacity>
)
}
}
You should pass an extraData prop to your FlatList so that it will rerender your items based on your selection
Here :
<FlatList style={styles.container}
data={this.state.data}
extraData={this.state.selectedItem}
renderItem={({ item }) => (
this.renderRow(item)
)}
/>
Source : https://facebook.github.io/react-native/docs/flatlist
Make sure that everything your renderItem function depends on is passed as a prop (e.g. extraData) that is not === after updates, otherwise your UI may not update on changes
First
constructor() {
super();
this.state = {
selectedIds:[]
};
}
Second
handleSelectionMultiple = async (id) => {
var selectedIds = [...this.state.selectedIds] // clone state
if(selectedIds.includes(id))
selectedIds = selectedIds.filter(_id => _id !== id)
else
selectedIds.push(id)
await this.setState({selectedIds})
}
Third
<CheckBox
checked={this.state.selectedIds.includes(item.expense_detail_id) ? true : false}
onPress={()=>this.handleSelectionMultiple(item.expense_detail_id)}
/>
Finally i got the solution to my problem from the answer given by Maicon Gilton