How to get currently visible index in RN flat list - javascript

I have a horizontal flat list where each item is width:300
All I am trying to do is to get index of currently visible item.
<FlatList
onScroll={(e) => this.handleScroll(e)}
horizontal={true}
data={this.state.data}
renderItem...
Tried this:
handleScroll(event) {
let index = Math.ceil(
event.nativeEvent.contentOffset.x / 300
);
And something like this:
handleScroll(event) {
let contentOffset = event.nativeEvent.contentOffset;
let index = Math.floor(contentOffset.x / 300);
but nothing is accurate I always get one index up or one index down.
What am I doing wrong and how to get correct current index in flat list?
For example I get to list item that is 8th in a list but I get index 9 or 10.

UPD. thanks to #ch271828n for pointing that onViewableItemsChanged shouldn`t be updated
You can use FlatList onViewableItemsChanged prop to get what you want.
class Test extends React.Component {
onViewableItemsChanged = ({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
}
render () => {
return (
<FlatList
onViewableItemsChanged={this.onViewableItemsChanged }
viewabilityConfig={{
itemVisiblePercentThreshold: 50
}}
/>
)
}
}
viewabilityConfig can help you to make whatever you want with viewability settings. In code example 50 means that item is considered visible if it is visible for more than 50 percents.
Config stucture can be found here

With related to #fzyzcjy's and #Roman's answers. In react, 16.8+ you can use useCallback to handle the changing onViewableItemsChanged on the fly is not supported error.
function MyComponent(props) {
const _onViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
}, []);
const _viewabilityConfig = {
itemVisiblePercentThreshold: 50
}
return <FlatList
onViewableItemsChanged={_onViewableItemsChanged}
viewabilityConfig={_viewabilityConfig}
{...props}
/>;
}

Many thanks to the most-voted answer :) However, it does not work when I try it, raising an error saying that changing onViewableItemsChanged on the fly is not supported. After some search, I find the solution here. Here is the full code that can run without error. The key point is that the two configs should be put as class properties instead of inside the render() function.
class MyComponent extends Component {
_onViewableItemsChanged = ({ viewableItems, changed }) => {
console.log("Visible items are", viewableItems);
console.log("Changed in this iteration", changed);
};
_viewabilityConfig = {
itemVisiblePercentThreshold: 50
};
render() {
return (
<FlatList
onViewableItemsChanged={this._onViewableItemsChanged}
viewabilityConfig={this._viewabilityConfig}
{...this.props}
/>
</View>
);
}
}

this.handleScroll = (event) => {
let yOffset = event.nativeEvent.contentOffset.y
let contentHeight = event.nativeEvent.contentSize.height
let value = yOffset / contentHeight
}
<FlatList onScroll={this.handleScroll} />
Get the round-off value to calculate the page-number/index.

In your case, I guess, you might ignore the padding or margin of items. The contentOffsetX or contentOffsetY should be firstViewableItemIndex * (itemWidth + item padding or margin).
As other answers, onViewableItemsChanged would be a better choice to meet your requirement. I wrote an article about how to use it and how it is implemented.
https://suelan.github.io/2020/01/21/onViewableItemsChanged/

A bit late, but for me was different with functional components...
If you are using functional component do it like:
const onScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const index = event.nativeEvent.contentOffset.x / slideSize;
const roundIndex = Math.round(index);
console.log("roundIndex:", roundIndex);
}, []);
Then on your FlatList
<FlatList
data={[2, 3]}
horizontal={true}
pagingEnabled={true}
style={{ flex: 1 }}
showsHorizontalScrollIndicator={false}
renderItem={renderItem}
onScroll={onScroll}
/>

While onViewableItemsChanged works for some conditions, it gives the
Error: Changing onViewableItemsChanged on the fly is not supported
error when ever you try to set the state.
This issue is fixed by 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>
);
}
Checkout this link for more details.

this will solve
Invariant Violation: Changing viewabilityConfig on the fly is not
supported
, and type script error on viewabilityConfigCallbackPairs.
const onViewableItemsChanged = ({ viewableItems }) => {
console.log(viewableItems)
};
const viewabilityConfig = { itemVisiblePercentThreshold: 100 };
const viewabilityConfigCallbackPairs = useRef([
{ viewabilityConfig, onViewableItemsChanged },
]);
<FlatList
...
viewabilityConfigCallbackPairs={
viewabilityConfigCallbackPairs.current
}
//viewabilityConfig={{ itemVisiblePercentThreshold: 100, }}<--------remove this
/>
typeScript:
import {ViewToken} from "react-native";
const onViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => {
...

Kindly use class-based components. onViewableItemsChanged() method only works with classes otherwise it will give an error

Related

React-Native Flatlist all Flatlist Rerender on one Item Selection

I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !

Show updated variable on another screen React Native

I have my ScreenOne.js file where I have a variable called points with a starting value of 0. I also have some functions in the file that add a value of 10 to the points.
The updated points variable is shown on the ScreenOne and it is updating well. I also want to display the updated points variable in a screen called ScreenTwo.js but that seems to be a problem.
I have already tried to store the updated value in a local storage but it returns null. In this case I try to export the updated variable but it return the variable that has the starting value instead of the updated value.
What could be the best way to transfer and display the updated variable in ScreenTwo?
ScreenOne:
const ScreenOne = ({ navigation }) => {
let valuePoints = 0
module.exports.valuePoints = valuePoints;
if (userInput1.search('No') < 0) {
console.log('Wrong answer!')
} else {
valuePoints += 10
}
if (userInput2.search('Yes') < 0) {
console.log('Wrong answer!')
} else {
valuePoints += 10
}
return (
<ScreenContainer>
<ScrollView style={{ flex: 1, width: '100%' }}>
<PointsAmount>
{valuePoints} //Here it displays 20
</PointsAmount>
</ScrollView>
</ScreenContainer>
)
}
export default ScreenOne;
ScreenTwo:
const ScreenTwo = ({ }) => {
const {valuePoints} = require('../screens/ScreenOne');
return (
<PointsContainer>
<ScrollView style={{flex:1, width: '100%'}}>
<TouchableOpacity onPress={() => navigation.navigate('MyTabs1')} >
<ScoreCard header='Points gained' points={valuePoints} goal={100} /> //Here it should display 20 but instead it displays 0
</TouchableOpacity>
</ScrollView>
</PointsContainer>
)
}
export default ScreenTwo;
You should have a look at the docs for the context in react.
You need to create a context that will be wrapping your App.
const [points, setPoints] = useState(0);
<Context.Provider value={{points, setPoints}}>
Your app
</Context.Provider>
Then in your ScreenOne and ScreenTwo you can get and set the value with:
const {
points, setPoints
} = useContext(Context);

Setup problem encountered by wrapping a React-Beautiful-DND + React-Window virtual list with AutoSizer

When wrapping my virtual list <FixedSizeList> component (react-beautiful-dnd + react-window) with an <AutoSizer> component (react-virtualized-auto-sizer) I'm receiving the following error:
react-beautiful-dnd
A setup problem was encountered.
Invariant failed: provided.innerRef has not been provided with a HTMLElement.
The error doesn't occur if I don't wrap the <FixedSizeList> component with <AutoResizer> and supply hard-coded values instead.
My program implements 2 separate, non-draggable lists that I can drag-and-drop to and from. Becasue the lists aren't draggable it's not a typical "board", but I used React-Beautiful-DND's CodeSandBox for React-Window Basic Board as a guide to make it work just as well.
List.js:
import { Draggable, Droppable } from "react-beautiful-dnd";
import { FixedSizeList } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import ListItem from "./ListItem";
import React, { memo, useCallback } from "react";
const List = ({
ID,
data
}) => {
const listItemRenderer = useCallback(({ data, index, style }) => {
const item = (data && data[index]);
if (!item) {
return null;
}
return (
<Draggable
key={item.ID}
draggableId={item.ID}
index={index}
>
{(provided) =>
<ListItem
data={item}
draggableProvided={provided}
virtualStyle={style}
/>
}
</Draggable>
);
}, []);
return (
<Droppable
droppableId={ID}
mode={"virtual"}
renderClone={(provided, snapshot, rubric) => (
<ListItem
data={data[rubric.source.index]}
draggableProvided={provided}
isDragging={snapshot.isDragging}
/>
)}
>
{(provided, snapshot) => {
const dataLength = (data)
? data.length
: 0;
const itemCount = (snapshot.isUsingPlaceholder)
? dataLength + 1
: dataLength;
return (
<AutoSizer> //Error here caused by wrapping <FixedSizeList> with <AutoSizer>
{({ width, height }) => (
<FixedSizeList
width={width} //AutoSizer supplied value
height={height} //AutoSizer supplied value
itemSize={100}
itemCount={itemCount}
itemData={data}
outerRef={provided.innerRef}
>
{listItemRenderer}
</FixedSizeList>
)}
</AutoSizer>
);
}}
</Droppable>
);
};
export default memo(List);
ListItem.js:
import React, { memo } from "react";
const ListItem = ({
data,
draggableProvided,
virtualStyle
}) => {
return (
<div
{...draggableProvided.draggableProps}
{...draggableProvided.dragHandleProps}
ref={draggableProvided.innerRef}
style={{
...draggableProvided.draggableProps.style,
...virtualStyle
}}
>
{data.name}
</div>
);
};
export default memo(ListItem);
Regardless of the error, everything seems to still function as it should, but I'd really like to understand the problem before moving forward.
I dug into the AutoSizer component to find the answer.
The error was logged because the children property of the AutoSizer HOC was not being rendered since the width and height values were 0. This is also why everything still functioned normally, as the width and height state values were eventually updated, but only after the initial render.
AutoSizer (index.esm.js):
// Avoid rendering children before the initial measurements have been collected.
// At best this would just be wasting cycles.
var bailoutOnChildren = false;
if (!disableHeight) {
if (height === 0) {
bailoutOnChildren = true;
}
outerStyle.height = 0;
childParams.height = height;
}
if (!disableWidth) {
if (width === 0) {
bailoutOnChildren = true;
}
outerStyle.width = 0;
childParams.width = width;
}
return createElement(
'div',
{
className: className,
ref: this._setRef,
style: _extends({}, outerStyle, style) },
!bailoutOnChildren && children(childParams)
);
Therefore, the solution is to supply defaultWidth and defaultHeight props with non-zero value to ensure that the component renders on mount, albeit with non-automated sizes:
//...
return (
<AutoSizer
defaultWidth={1}
defaultHeight={1}
>
{({ width, height }) => (
<FixedSizeList
width={width}
height={height}e
itemSize={100}
itemCount={itemCount}
itemData={data}
outerRef={provided.innerRef}
>
{listItemRenderer}
</FixedSizeList>
)}
</AutoSizer>
);

react virtual list updatable item count

Libraries like react-virtualized, react-window and react-virtuoso have item count property like in code below from materal-ui. However it is located within return. Is there any way to make item counterupdatable?
export default function VirtualizedList() {
const classes = useStyles();
return (
<div className={classes.root}>
<FixedSizeList height={400} width={300} itemSize={46} itemCount={200}>
{renderRow}
</FixedSizeList>
</div>
);
}
Yes you can pass on a dynamic value to the itemCount property in FixedSizeList. It take care of it and also ensure that the scroll remain where it is currently
A sample code would look like
const Example = () => {
const [rowCount, setRowCount] = useState(10);
useEffect(() => {
setTimeout(() => {
console.log("changed");
setRowCount(1000);
}, 10000);
}, []);
console.log(rowCount);
return (
<List
className="List"
height={150}
itemCount={rowCount}
itemSize={35}
width={300}
>
{Row}
</List>
);
};
Working demo

Why FlatList is not updating dynamically in React Native

I'm very new to react native and I'm trying to update list dynamically.
Below is my code:
import React, { Component } from "react";
import { View, Text, StyleSheet, FlatList } from "react-native";
import { Tile, ListItem, List } from "react-native-elements";
export default class JoinSession extends Component {
constructor() {
super();
this.state = {
dataToRender: [{ "id": "0", "name": "name0", "des": "des0" }]
}
}
componentDidMount() {
var counter = 0;
const interval = setInterval(() => {
try {
var temp = {
"id": ++counter + "",
"name": "name" + counter,
"des": "des" + counter
}
let tempArray = this.state.dataToRender;
tempArray.push(temp);
this.setState({
dataToRender: tempArray
});
console.log(this.state.dataToRender);
if(counter === 10) {
clearInterval(interval);
}
} catch (e) {
console.log(e.message);
}
}, 2000);
}
renderList(item) {
console.log(item);
return (
<ListItem
roundAvatar
title={item.name}
subtitle={item.des}
/>
);
}
render() {
return (
<View style={{ flex: 1, alignItems: "stretch", backgroundColor: "skyblue" }}>
<List>
<FlatList
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
</List>
</View>
);
}
}
I am only able to get first element which I've declared in the constructor but data which I'm appending in serInterval is not showing up on the page.
Please help me to resolve it or if there is any other way to do it, please let me know.
Thanks in advance.
Hi try to have a look on the extraData parameter you can use on a FlatList:
By passing extraData={this.state} to FlatList we make sure FlatList itself will re-render when the state.selected changes. Without setting this prop, FlatList would not know it needs to re-render any items because it is also a PureComponent and the prop comparison will not show any changes.
<FlatList
...
extraData={this.state}
/>
More info on the FlatList documentation: https://facebook.github.io/react-native/docs/flatlist.html
Also you shouldn't need this <List> from react native element here the list behaviour is totally handle by your FlatList.
Like say #AlexDG Flat list is Pure component. For updating pure component use key prop.
But do not overdo it, otherwise you can get unnecessary redrawing.
<FlatList
key={this.state.dataToRender.length} <---------- rerender
data={this.state.dataToRender}
renderItem={({ item }) => this.renderList(item)}
keyExtractor={item => item.id}
/>
I just had this on my own and just so happened to read this comment by OP:
The problem was array mutation. Never mutate array or object in react native.
And I indeed changed my state this way:
this.setState(prev =>
prev.listData.push("stuff");
return prev;
});
And you can see it in the question as well:
let tempArray = this.state.dataToRender;
tempArray.push(temp);
After changing it to
this.setState(prev => {
let copy = Array.from(prev.listData);
copy.push("stuff");
return {listData: copy};
});
however, my list was updating just fine!
So if you are mutating an array in your state that is related to your list's data you might want to see if this helps some.

Categories

Resources