Using FlatList#onViewableItemsChanged to call a Component function - react-native

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.

Related

How to create an rxjs Observable from TextInput (either onChange or onTextChange)

I want to create an observable from a change event that gets fired on a React Native TextInput component. TextInput comes with 2 change props that I'm aware of (onChangeText and onChange). From what I gather, you need to use onChange if you want access to the native event you need to use onChange.
I don't know much about the native event object. I am trying to create an rxjs observable using fromEvent.
First I created a ref in my functional component like this:
const sqftRef = useRef().current
Then I attached this ref to the TextInput component like this:
<TextInput
ref={sqftRef} // attach a ref
label='Sqft'
mode='flat'
textContentType='none'
autoCapitalize='none'
keyboardType='numeric'
autoCorrect={false}
value={String(formValues.sqft)}
dense
underlineColor={colors.colorOffWhite}
onChangeText={(text) => setText(text)}
onChange={e => {
// somehow create an observable from this event ???
}}
style={styles.inputStyles}
theme={inputTheme}
/>
I tried to create an Observable using fromEvent like this but it doesn't work. I get undefined is not an object (evaluating target.addEventListener):
fromEvent(sqftRef, 'onChange').subscribe(value => console.log(value))
I know my approach is all wrong. Hoping someone can point me in the correct direction.
I would emit events you need into a subject, then subscribe to the subject in other parts of your code.
Here's a simple React example that should get you started
function App() {
const textChange = new Subject<string>();
useEffect(() => {
// subscribe to
const subscription = textChange.asObservable().subscribe(console.log)
return () => subscription.unsubscribe()
}, [])
// Emit events with a subject
return <textarea onChange={(e) => {
textChange.next(e.target.value)
}}>
</textarea>
}
render(<App />, document.getElementById('root'));
Check out the example here: https://stackblitz.com/edit/react-ts-akoyfv
I think the problem is with assigning the current directly to the sqftRef. Try to define it without current, but use current when creating the Observable, like the following:
const sqftRef = useRef();
Then create the Observable within useEffect to make sure that the DOM is ready:
useEffect(() => {
fromEvent(sqftRef.current, 'onChange').subscribe((value) =>
console.log(value)
);
});
OK, I was able to figure it out with the help of Amer Yousuf and Alex Fallenstedt.
I did something similar to what Alex suggested, modifying his solution for React Native. One reason his solution wasn't working for me is that it is important to use the useRef hook to prevent the Observable from being re-created on each render. If the observable is recreated (on a re-render) and useEffect doesn't run again, then we won't have an active subscription to the newly (re-created) observable (useEffect never runs again). That's why my call to sqft$.next was originally only being called once (the first time until we re-render).
My solution looks like this:
let sqft$ = useRef(new BehaviorSubject(0)).current
useEffect(() => {
const sub = sqft$.subscribe({
next: (val) => {
// just testing stuff out here
updateForm('sqft', val)
updateForm('lot', val * 2)
}
})
// this is only relevant to my use case
if (activeReport) sqft$.next(activeReport.sqft)
return () => sub.unsubscribe()
}, [activeReport])
and of course I call this in onChangeText:
onChangeText={(text) => {
sqft$.next(text)
}}
So this is working right now. I still feel like there may be a better way using onChange(e => ...stuff). I will leave this question open for a little bit in case anyone can break down how to do this using nativeEvent or explain to me how I can access an event off the TextInput component.

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.

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

React Native FlatList Not Re-Rendering after Asyncronous operation

I have an async function like this:
getDeals() {
if(this.props.user) {
this.setState({loading: true});
this.setState({deals: []});
var parameters = {
zip: this.props.user.zip,
sort: 'All',
category: this.props.selectedCategory,
company: this.props.user.company,
page: null,
user: this.props.user,
search: null
}
axios.post(`${constants.api}/grab-deals/`, parameters)
.then((response) => {
this.setState({totalDeals: response.data.length});
this.setState({deals: response.data, loading: false, refreshing: false});
this.forceUpdate();
})
}
}
And a FlatList component Like this:
<FlatList data={this.state.deals} style={{flex: 1, padding: 10}} extraData={this.state} keyExtractor={this.keyExtractor} renderItem={this.renderDeal.bind(this)} />
Here is the keyextractor:
keyExtractor = (item, index) => item.id;
When I call this.getDeals() the first time it works great. However when I call it a second time the axios call get's all of the correct data, but the flat list still keeps old data (it doesn't remove items that aren't in the new call).
How do I get the FlatList to always reflect the returned data?
Call this.getDeals() in componentWillUpdate() and update props?
I believe you confussing what props and state is for. Basically state is used for things that could change during the lifecycle of the component and props are kept immutable. I use them for behavior.
Unless you are changing the parameters for the getDeals function on the second call, see that all of the properties are based on the props, which are not always updated.
RN has a method called componentWillUpdate that is triggered with the new props which you can then be used to update the component itself. If you want to keep using props in your getDeals method, you will need to check if the props have changed (this happens when the parent updates the child with new props) and then trigger again the data fetch.
If this does not help, please post more code.
According to the docs you need to set the state.selected value
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.

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]