React Native mobx binding to FlatList not working - mobx

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]

Related

React Native flatlist inverted

I have a flatlist that shows a messages chat. I want to start at the bottom of the flatlist (showing the most recent messages). I added 'inverted={true}' and 'data={data.reverse()}' as the props.
It is sort of working, but the data alternates being reversed and not reversed every time i leave and enter the chat again.
Anyone know the fix?
Attached code
.reverse() mutates the original list.
If you do want your list reversed, do it like this
const App = ({ data }) => {
const reversed = [...data].reverse(); // Make a shallow copy, then reverse the copy
return <Flatlist data={reversed} inverted />;
};
However, you will probably notice that you don't want your list reversed, because inverted is already reversed.
const App = ({ data }) => {
return <Flatlist data={data} inverted />;
};

React Native doesn't re-render on DOM change

I had an array of components inside a ScrollView component. Somehow react native doesn't re-render when the array is modified.
Here's a demonstration of my problem:
const TestApp = () => {
const [arr, setArr] = useState([]);
function pushArr() {
setArr((arr) => {
arr.push(1);
return arr;
});
console.log('pushArr():', arr);
}
function flushArr() {
setArr([]);
console.log('flushArr():', arr);
}
useEffect(() => {
console.log('useEffect():' , arr);
})
return (
<>
<ScrollView style={{flex:1}}>
{arr.map((elem, i) => <Text key={i}>{elem}</Text>)}
</ScrollView>
<Button title="Push" onPress={pushArr}></Button>
<Button title="Flush" onPress={flushArr}></Button>
</>
)
}
The page remains blank, and no updates happen on button press.
I've logged out arr and these are my findings:
pushArr() and flushArr() works as expected
useEffect() gets triggered only on startup and after flushArr()
Can anyone explain this behavior, and what mistakes have I made?
If I remember correctly, you need to make a copy of the array whenever you want it to “react”. The new memory address will let react know it should update. In other words, you shouldn’t mutate the array.
You can use the spread operator to make a copy and then push an element to the end which you can then pass to useArr. Usually I see people just passing the new object inside your useArr function.
I also don’t see you passing anything to your useArr function.

Render FlatList of Videos in a performant way

I am using a react native with expo. I have a lot of videos that I need to render (sort of like TikTok does). When I fetch about 30 videos and put them in the flat list in the renderItem method, it gets stuck and luggish. I was thinking about getting an amount of videos but sending to the renderItem method only 3 videos each time, and when the user will scroll down and reach index 2 it will shift the first index and append the fourth video from the fetched one. The idea was to have a small array of size 3 and change the items in it every scroll, in order to prevent rendering all the videos at once. That required array manipulation and caused a rerender each time the array of videos was updated(each change made sort of a flash - what was indicating a whole rerender).
My question is how should it be implemented in order the transition between the videos to be as fast and clean as possible from the client side perspective? What is the correct way to render videos in a flat list so it won't be stuck? I dont think It should be done that way, there has to be a better way.
This is what I have tried:
// challenges is an array coming from a fetch, just sliced it for the purpose of the example
// suppose it is an array that contains 30 items
const [currentVideos, setCurrentVideos] = useState([challenges.slice(0,3)]);
<FlatList
data={currentVideos}
renderItem={renderItem}
keyExtractor={(challenge, i) => challenge._id}
showsVerticalScrollIndicator={false}
snapToInterval={Dimensions.get("window").height - UIConsts.bottomNavbarHeight}
snapToAlignment={"start"}
decelerationRate={"fast"}
ref={(ref) => {
flatListRef.current = ref;
}}
onScrollToIndexFailed={() => alert("no such index")}
onViewableItemsChanged={onViewRef.current}
onScrollEndDrag={() => (scrollEnded.current = true)}
onScrollBeginDrag={beginDarg}
></FlatList>
useEffect(() => {
// just wanted to check on 3 videos
if (currentlyPlaying === 2) {
let temp = currentVideos;
temp.shift(); // pop the top item
temp.push(challenges[4]) // append a new one
setCurrentVideos(temp);
}
}, [currentlyPlaying]);
const onViewRef = useRef(({ viewableItems }) => {
// change playing video only after user stop dragging
scrollEnded.current && setCurrentlyPlaying(viewableItems[0]?.index);
});
I would avoid manipulating the data array and doing business logic inside of the component.
Besides, you can achieve your desired behaviour without the need to manipulate your data array at all, with the maxToRenderPerBatch FlatList prop. As mentioned in the official RN docs for FlatList optimization techniques.
You should avoid using anonymous functions and objects inside of your component's properties, move them outside of the return statement and use the useMemo and useCallback hooks to avoid their unnecessary recreation on every re-render. For example instead of writing your code like this:
const App = () => {
return (
<FlatList
keyExtractor={(challenge, i) => challenge._id}
snapToInterval={Dimensions.get('window').height - UIConsts.bottomNavbarHeight}
/>
);
};
A better approach would be to re-write it to something like this:
const App = () => {
// Because of useCallback, the keyExtractor function will be memoized and won't recreate itself on every re-render
const keyExtractor = useCallback((challenge, i) => challenge._id, []);
// useMemo is almost the same as useCallback, but it is used to return non-function types
// Defining your snapToInterval variable like this will cause it to memoize its value and it
// won't recreate itself on every re-render
const snapToInterval = useMemo(() => Dimensions.get('window').height - UIConsts.bottomNavbarHeight, []);
return (
<FlatList
keyExtractor={keyExtractor}
snapToInterval={snapToInterval}
/>
);
};
If you haven't already, you should consider extracting the component returned from the renderItem function to a different file and applying React.memo to it.
Note: try not to overuse useCallback and useMemo. You can find good and detailed explanation of why not to overuse them here and here.
If you're able to, you should optimize your videos before uploading them to the server. You can optimize your client side part of the app as much as you want, but if the content isn't properly optimized, you won't be able to achieve a smooth and performant experience regardless of your efforts.
Here's also some articles describing how you can optimize your FlatList component:
How did I optimize my React Native FlatList?
8 ways to optimize React native FlatList performance
Optimizing a React Native FlatList With Many Child Components
React Native Performance Optimisation With Hooks
React Native: Optimized FlatList of videos
I hope that some of this will be helpful to you. Good luck.
I have been searching for a solution as well. I have worked out a solution based on some previous work using InViewPort. you can check it out here https://github.com/471Q/React-Native-FlatList-Video-Feed

getItemLayout in FlatList passing index -1 on first render

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
}
/>

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.