getItemLayout in FlatList passing index -1 on first render - react-native

I am implementing a FlatList with initialScrollIndex and getItemLayout. However, every time my app starts up, it renders the first elements somehow and then jumps to the actual initialScrollIndex. This means that the actual performance boost that I am supposed to get, isn't working.
When checking the getItemLayout function I can see that when it renders the first index that is passed is -1 instead of the initialScrollIndex, thus throwing an error and breaking. The other random indexes are passed untilinitialScrollIndex` is passed.
Any ideas why this might be happening?
FlatList:
renderMonthPerMonth() {
const data = this.deriveMonthPerMonthDataFromProps();
const initialScrollIndex = this.deriveInitialScrollIndex();
return (
<FlatList
data={data}
ref={'flatlist'}
initialNumToRender={3}
onLayout={this.onLayout}
getItemLayout={this.getItemLayout}
showsVerticalScrollIndicator={false}
initialScrollIndex={initialScrollIndex}
renderItem={this.renderOneMonthPerMonth}
ListHeaderComponent={this.renderListHeader}
keyExtractor={el => `${el.monthName}-monthPerMonth`}
onScrollBeginDrag={() => this.onExpandMenu('scroll')}
ItemSeparatorComponent={this.renderItemSeparator}
ListFooterComponent={this.renderFooterComponent}
/>
);
}
getItemLayout:
getItemLayout(data, index) {
const monthAsNumber = moment().month(data[index].monthName).format('M')-1;
const SEPARATOR_HEIGHT = 25;
const MONTH_NAME_CONTAINER_HEIGHT = 55;
const ONE_DAY_HEIGHT = (((width-40)/7)/1.1);
const WEEKS_IN_MONTH = this.weeksInMonth(moment().format('YYYY'), monthAsNumber);
// define oneMonthHeight using weeksInMonth method
const oneMonthHeight = (ONE_DAY_HEIGHT * WEEKS_IN_MONTH) + (MONTH_NAME_CONTAINER_HEIGHT + SEPARATOR_HEIGHT);
return {
length: oneMonthHeight,
offset: oneMonthHeight * index,
index,
}
}
Console:

I have a suspicion the problem is in the renderItem prop and subsequently the renderOneMonthPerMonth() method.
Referring to the FlatList docs:
This is a PureComponent which means that it will not re-render if props remain shallow-equal. 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. This includes the data prop and parent component state.
and
renderItem({ item: Object, index: number, separators: { highlight: Function, unhighlight: Function, updateProps: Function(select: string, newProps: Object) } }) => ?React.Element
Having this.deriveMonthPerMonthDataFromProps() may not be Pure (depending on that prop, I cannot see it).
Try fetching all the data and assigning to a const outside the renderItem method that is passed. Alternatively if this data is pure and not changing you can skip this step.
The next two are probably the crux of the issue: create a render function resembling this:
const renderItem = ({ item, index }) => {
return <DummyComponent item={item} index={index} /> // item and index are not necessarry just demonstrating
}
Ensure you have a keyExtractor returning unique identifier. Else this will error in a similar manner to your issue. Try the below as a sanity check
keyExtractor={(item, index) => index.toString()}
If the above is not the issue you also need to ensure that your data prop on your Flatlist is an array and not an Object or other data structure as explained in the data section of the Flatlist docs.
If your data is of type object and you don't rely on the keys to access specific pieces of data in other methods, you can change the initial data to be an array of objects like so:
[
{ ...monthOneData },
{ ...monthTwoData },
{ ...monthThreeData }
]
Or, if you want to keep the original data as an object you can convert it to an array of keys by using
Object.keys(this.state.data)
Then in your FlatList you could do something like this:
<FlatList
data={Object.keys(this.state.data)}
renderItem={({item}) =>
<DummyComponent month={this.state.data[item].month} /> // item is now the key value of the objects (almost like a look up table)
// also not having curly braces implies an implicit return in this case
}
/>

Related

Why is data from useQuery undefined when I start screen?

When I start screen, I get data from useQuery
And by using useEffect, I get data and immediately sort this data.
After sorting data, I put this data to useState named flatlistdata.
And I draw screen by using Flatlist with this data.
Flatlist data should be sorted.
So I need to refined data before.
const { data: allFeedData, refetch: allFeedRefetch } = useQuery(SEE_ALL_FEED_ORDER);
const [flatlistdata, setFlatlistdata] = useState([]);
useEffect(() => {
allFeedRefetch();
setFlatlistdata(
[...allFeedData.seeAllFeedOrder].sort(function (a, b) {
return b.directFeedNumber - a.directFeedNumber;
})
);
}, []);
<FlatList
data={flatlistdata}
keyExtractor={(item) => item.id}
renderItem={RankRow}
refreshing={refreshing}
onRefresh={refresh}
/>
However when I click screen, it says undefined is not an object which means data is empty.
I think the problem here is that screen is drawn, before sorting data and putting it to useState...?
I need a little hint 😭
Please help me.
The query is an asynchronous operation, so, the data variable will always start out as undefined while the network request completes. There are two possible solutions.
In your useEffect, only set your state if data is defined. Wrap your setFlatlistData with if (allFeedData?.length) { ... }
Use the React Query selection prop to have Query do the same operation before allFeedData gets populated. Check the select prop in the docs for useQuery: https://react-query-v2.tanstack.com/reference/useQuery

Confusing React Native syntax

How would "item" know that it has the data from the Data properties? I mean I passed in the data using the data properties of Flatlist component and it seems I can give any name to the keyExtractor property and it will know to get the data from the data property. Is there any explanation for this? Thanks a lot and in advance.
<Flatlist
data={someData}
keyExtractor={item => item.TitleID}/>
I'll give you a briefly introduction and an example of what callback functions are and how to use them.
Basically you can define a function and it's parameters with any types. By that, you can also expect functions as parameters (those are also called higher order functions) and call them inside the parent function at any time you wish. Look at this and especially at the lambda calculus.
function bar(something) {
console.log(something);
}
function foo(callback) {
callback("Hello");
}
// This will generate the output 'Hello'.
foo(bar);
// This is the same, but using lambda calculus.
foo(something => console.log(something));
As you can see, you can pass function definitions as parameters to other functions. This is exactly, what your example is doing. I also give you an extra example.
import React from "react";
const FlatList = ({ keyExtractor, data, ...props }) => {
return (
<ul>
{data.map((item, index) => (
<li key={index}>{keyExtractor(item)}</li>
))}
</ul>
);
};
export default FlatList;
Here I tried to mockup the FlatList of your example. You can see the data.map call, which acts as the higher order function from the previous example. The FlatList component has a property called keyExtractor, which will be called for each element of the data property. Now, you can easily provide keyExtractor as follows.
const someData = [
{ name: "Something", id: 1 },
{ name: "Anything", id: 2 }
];
//...
<FlatList
data={someData}
keyExtractor={item => item.id}
/>
This will render an unordered list, which contains the id of every element of someData. In this case it is just a projection of your data.
Here the executable example https://codesandbox.io/s/laughing-paper-lmi22.
Cheers
Under the hood, FlatList passes your data item to the keyExtractor.
Here your function myKeyExtractor accepts a parameter, which is basically the reference to your data.
function myKeyExtractor(item) {
return item.TitleID;
}
<Flatlist
data={someData}
keyExtractor={myKeyExtractor}
/>
Let's take a simple example here.
It is similar to the parameters of add and subtract functions here.
You could name the parameters anything you like. Parameters are just placeholders for your value.
function add(x, y) {
return x + y;
}
function subtract(thisIsAFirstParameter, thisIsASecondParamater) {
return thisIsAFirstParameter - thisIsASecondParamater;
}
add(10, 5);
subtract(10, 5);

What is renderRow ()

I went through React-Native docs to figure out what is renderRow() but for some reason I am unable to comprehend what does it say from Facebook React-Native docs
This what the official docs says
renderRow
(rowData, sectionID, rowID, highlightRow) => renderable
Takes a data entry from the data source and its ids and should return a renderable component to be rendered as the row. By default the data is exactly what was put into the data source, but it's also possible to provide custom extractors. ListView can be notified when a row is being highlighted by calling highlightRow(sectionID, rowID). This sets a boolean value of adjacentRowHighlighted in renderSeparator, allowing you to control the separators above and below the highlighted row. The highlighted state of a row can be reset by calling highlightRow(null).
[Question:] Can someone please explain me this with example?
ListView is deprecated, use FlatList instead with the equivalent renderItem method. This is responsible of the actual rendering of each row based on the data records:
const data = [
{ key: '1', label: 'foo' },
{ key: '2', label: 'bar' }
]
renderTheItem = ({item}) => {
return <Text>{item.label}</Text>
}
<FlatList
data={data}
renderItem={this.renderTheItem}
/>
And the rendered result will be something like this:
<View> --> coming from FlatList wrapper
<Text key="1">foo</Text> --> coming from the custom renderTheItem function
<Text key="2">bar</Text>
</View>
It is mandatory to either add a unique key prop for each data record, or define a keyExtractor function. Also important to destruct the item in the renderer function with ({item}) as it has other meta parameters as written in documentation of FlatList.
renderItem({ item, index, separators}) => {}

Using FlatList#onViewableItemsChanged to call a Component function

I'm currently attempting to implement a form of LazyLoading using the FlatList component, which introduces a neat little feature called onViewableItemsChanged which gives you a list of all of the components that are no longer on the screen as well as items that are now on the screen.
This is a custom LazyLoad implementation and as such is more complicated than most LazyLoad open-sourced libraries that are available, which is why I'm working on my own implementation. I'm already looked into react-native-lazy-load and others.
Basically, I need to be able to call a function that's part of the component being rendered in the FlatList, I've tried creating a reference to the item rendered in the FlatList and calling it as such, but it doesn't seem to work.
For example:
<FlatList data={...}
renderItem={(item) => <Example ref={(ref) => this[`swiperRef_${item.key}`] = ref}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
onViewableItemsChanged = ({viewableItems}) => {
viewableItems.forEach((item) => {
const { isViewable, key } = item;
if(isViewable && !this.cachedKeys.includes(key)) {
const ref = this[`swiperRef_${key}`];
if(!ref) return console.error('Ref not found');
ref.startLoading();
this.cachedKeys.push(key);
}
});
}
Now in the <Example /> component I would have a function called startLoading which should be called when a new visible item is brought onto the screen, however the ref never exists.
I was actually doing everything correctly, but I accidently forgot to deconstruct the parameter returned from the renderItem function, so (item) should have been ({ item })
That's all there was to it.

React Native mobx binding to FlatList not working

I have a RN (0.44.2) mobx (3.1.10) app which uses a FlatList. I'm basically following https://blog.callstack.io/write-react-native-apps-in-2017-style-with-mobx-e2dffc209fcb
When using my own store, opposed to the examples, I'm having to use toJS() in order to get the FlastList to render
// renders list
<FlatList
data={this.props.giphyStore.images.toJS()}
keyExtractor={(_, i) => i}
renderItem={({ item }) => <Text>found the data</Text>}
/>
// does not render list
<FlatList
data={this.props.giphyStore.images}
keyExtractor={(_, i) => i}
renderItem={({ item }) => <Text>did not find the data</Text>}
/>
I'm really struggling to figure out why toJS() might be needed in some cases and not others.
My store is setting the images observable like this
async getImageList(query: string) {
try {
const requestURL = `${constants.GIPHY_ENDPOINT}${query}`
const response = await axios.get(requestURL);
const imgs = response.data.data.map((item) => {
return { id: item.id, url: item.images.downsized.url }
})
this.images.replace(imgs)
} catch (e) {
}
}
As a follow up question, I'm not sure why I need to do the following this.images.replace(imgs) where as in the tutorial he simply did does this.tracks = response.data.tracks.items which triggers the observable just fine.
If anyone has suggestions, I would very much appreciate it.
This is because mobx's arrays are objects and the data in FlatList or in react native expects an array. You can read more about it in here and there.
Also..., slice returns a shallow copy; a new array with the same contents, while toJS also converts the values inside the array (but only if they are observables).
This question is kinda old, but it's also worth mentioning that MobX only tracks the render function by default, while FlatList accepts rendering callbacks and calls them. (eg renderItem={this.renderItem})
In order for items to update without the whole list refreshing, wrap the render callback's result with <Observer>.
See Understanding reactivity [Mobx docs]