React Native FlatList flickers lazy loading additional data - reactjs

I have a fairly basic FlatList component implemented using hooks. The list simply loads data from a random user API and lazy loads additional data via infinite scroll. The only visual issue I'm experiencing is that when I merge the new data with the current, the new data being appended flickers very briefly before fully rendering. Not sure what could be causing this.
Expo Snack
const useRestApi = (url) => {
const [ data, setPeople ] = useState([]);
const [ page, setPage ] = useState(1);
const [ results, setResults ] = useState(20);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
const fetchPeople = async () => {
setLoading(true);
const response = await fetch(`${url}&page=${page}&results=${results}`);
const json = await response.json();
if(page !== 1)
setPeople([...data, ...json.results]);
else
setPeople(json.results);
setLoading(false);
}
fetchPeople();
}, [page]);
return [{data, loading, page}, setPage, setResults];
}
const App: () => React$Node = () => {
const [{ data: people, loading, page }, setPage, setResults] = useRestApi(`https://randomuser.me/api?&seed=ieee`);
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<FlatList
data={people}
onEndReachedThreshold={0.2}
keyExtractor={(item, index) => index.toString()}
renderItem={({item, index}) => (
<View key={index} style={styles.listItem}>
<Text style={styles.listItemHeader}>{item.name.first} {item.name.last}</Text>
<Text style={styles.listItemSubHeader}>{item.location.country}</Text>
<Text style={styles.listItemBody}>{item.location.street.number} {item.location.street.name}</Text>
<Text style={styles.listItemBody}>{item.location.city} {item.location.state} {item.location.postcode}</Text>
</View>
)}
refreshing={loading}
onRefresh={() => {setResults(20); setPage(1);}}
onEndReached={() => {setResults(5); setPage(page + 1);}}
ItemSeparatorComponent={() => ItemSeparatorComponent}
ListFooterComponent={() => loading ? ListFooterComponent : null}
/>
</SafeAreaView>
</>
);
};

Related

Getting "Can't perform a React state update on an unmounted component" only the first time

I am creating a ToDo app. This app has two screens: Todos and Done. I'm using BottomTabNavigator to switch between these screens. These two screens has list of todos. The todos component shows the undone todos and the Done component shows the done todos. There's a checkbox on the left and Trash icon on the right of every single todo. When a todo from Todos page is checked then it moves to the Done page. The issue is: after switching to the Done screen from Todos for the first time then after unchecking the todo there gives this warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in SingleTodo (at Done.tsx:74)
After this, the app is running perfectly. As I'm not sure which component is causing this error that'w why I'm sharing the minimal version of the code.
I have set up Bottom Tab navigator component like this:
import stuff..
...
const HomeTab = () => {
return (
<Tab.Navigator
screenOptions={({route}) => ({
headerShown: false,
tabBarIcon: ({focused, color, size}) => {
let iconName = '';
size = focused ? 25 : 20;
if (route.name === 'To-Do') {
iconName = 'clipboard-list';
} else if (route.name === 'Done') {
iconName = 'clipboard-check';
}
return <FontAwesome5Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0080ff',
tabBarInactiveTintColor: '#777777',
tabBarLabelStyle: {fontSize: 15, fontWeight: 'bold'},
})}>
<Tab.Screen name="To-Do" component={Todos} />
<Tab.Screen name="Done" component={Done} />
</Tab.Navigator>
);
};
export default HomeTab;
As you can see, there are 2 components here. One is Todos. The code for this component is as follows:
import stuff...
...
const Todos = ({navigation}) => {
const dispatch = useAppDispatch();
const {todos}: {todos: TodoInterface[]} = useAppSelector(
state => state.todoReducer,
);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = () => {
AsyncStorage.getItem('todos').then(todos => {
const parsedTodos: TodoInterface[] = JSON.parse(todos || '{}');
dispatch(setAllTodo(parsedTodos));
});
};
return (
<HideKeyboard>
<View style={styles.body}>
<FlatList
data={todos.filter(todo => todo.done !== true)}
renderItem={({item, index}) => {
const firstChild = index == 0 ? {marginTop: 5} : {};
return (
<TouchableOpacity
style={[styles.todoWrp, firstChild]}
onPress={() => todoPressHandler(item.todoId)}>
<SingleTodo // ***The code for this one is given below***
title={item.title}
subtitle={item.subTitle}
done={item?.done}
todoId={item.todoId}
/>
</TouchableOpacity>
);
}}
keyExtractor={(item, i) => item.todoId}
/>
<TouchableOpacity style={styles.addBtn} onPress={addBtnHandler}>
<FontAwesome5 name="plus" color="#fff" size={25} />
</TouchableOpacity>
</View>
</HideKeyboard>
);
}
The code for SingleTodo is as follows:
const SingleTodo = ({title, subtitle, done: doneProp, todoId}: Props) => {
const [done, setDone] = useState(doneProp);
const dispatch = useAppDispatch();
const {todos}: TodosType = useAppSelector(state => state.todoReducer);
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
return (
<View style={styles.body}>
<CheckBox
value={done}
onValueChange={val => checkBoxHandler(val)}
style={styles.checkbox}
/>
<View>
<Text style={[styles.title, GlobalStyle.IndieFont]}>{title}</Text>
<Text style={[styles.subtitle, GlobalStyle.IndieFont]}>{subtitle}</Text>
</View>
<View style={styles.trashWrp}>
<TouchableOpacity onPress={deleteHandler}>
<FontAwesome5Icon
style={styles.trashIcon}
name="trash"
color="#e74c3c"
size={20}
/>
</TouchableOpacity>
</View>
</View>
);
};
export default SingleTodo;
The code for Done component is similar to Todos component. The only changes is on the data property of the component
<FlatList
data={todos.filter(todo => todo.done === true)}
...
other props...
...
/>
It's happening every time you use this, it is just shown once to not spam the console.
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
Basically, you call the function, and the todo is unmounted from the state, but the function is not completed yet and you get that warning.
The solution is to lift everything related to the deleteHandler and checkBoxHandler from your children (Todo) to your parent (Todos), and pass it to Todo as props. Since parent is always mounted, deleting the todo will not unmount the parent and therefore, delete function will not be interrupted.

react_native + iframe + firebase : How could I serve the videoId of next index from realtime database?

now i use react native, expo project.
and i apply realtime database, iframe, FlatList.
and i use videoID instead of the 'scr= URL'.
i hope to autoplay by auto changing the videoId in my realtime database.
i think it can be possible by using index number.
and when i console.log selected index, it presented on terminal.
but i don't know how to apply that path for my code.
this is my realtime database.
enter image description here
and this is my codes.
import YoutubePlayer from "react-native-youtube-iframe";
const [state, setState] = useState([])
const [cardID, setCardID] = useState(["i4S5hvPG9ZY"])
const [playing, setPlaying] = useState(true);
const onStateChange = useCallback((state) => {
if (state === "ended") {
setPlaying(true)
}
}, []);
const onPress = ({ item, index }) => {
console.log({ index })
return (
setCardID(item.id.videoId)
)
}
...
useEffect(() => {
setLoading(true);
firebase_db.ref('/TGBSitems')
.once('value')
.then((snapshot) => {
console.log("TGBS에서 데이터 가져왔습니다!!")
let TGBSitems = snapshot.val()
setState(TGBSitems)
setTotalDataSource(TGBSitems);
setLoading(false);
})
.catch(err => { setLoading(false); setError(err); })
}, []);
...
return (
...
<View>
<YoutubePlayer
height={200}
play={playing}
videoId={cardID}
onChangeState={onStateChange}
// playList
/>
</View>
...
<FlatList
data={state}
// ItemSeparatorComponent={ItemSeparatorView}
keyExtractor={(index) => index.toString()}
renderItem={({ item, index }) => (
<View style={styles.cardContainer}>
<TouchableOpacity style={styles.card} onPress={() => onPress({ item, index })}>
<Image style={styles.cardImage} source={{ uri: item.snippet.thumbnails.medium.url }} />
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{item.snippet.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{item.snippet.description}</Text>
<Text style={styles.cardDate}>{item.snippet.publishedAt}</Text>
<Text style={styles.cardDate}>{item.id.videoId}</Text>
<Text style={styles.cardDate}>{index}</Text>
</View>
...

React infinite loop in useEffect()

I have a useEffect() which should be called once but continuously it is calling the getStories method.
const [pageNo, setPageNo] = useState(1);
const [recordsPerPage, setrecordsPerPage] = useState(5);
const { jwt } = useSelector((state: StateParams) => state.account);
useEffect(() => {
if (jwt) {
dispatch(
getStories({ token: jwt, pageNo: pageNo, recordsPerPage: recordsPerPage })
);
}
}, []);
UPDATED: I feel flatlist is causing
<FlatList
style={{
marginTop: 14,
alignSelf: "stretch"
}}
data={Items}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
const renderItem = useCallback(({ item } : {id: string}) => {
console.log({item})
const Section = Components[item.id]
return (<Section {...props}/>)
}, [Items])

React Native Deck Swiper

I am trying to make a GET request to an enpoint using the two functional components below to display in my react native deck swipper
//using fetch
const getDataUsingFetch = () => {
fetch(latestNews+ApiKey)
.then((response) => response.json())
.then((responseJson) => {
// set the state of the output here
console.log(responseJson);
setLatestNews(responseJson);
})
.catch((error) => {
console.error(error);
});
}
//using anxios
//asynchronous get request call to fetech latest news
const getDataUsingAnxios = async () => {
//show loading
setLoading(true);
setTimeout(async () => {
//hide loading after the data has been fetched
setLoading(false);
try {
const response = await axios.get(latestNews+ApiKey);
setLatestNews(response.data);
setLoading(false);
console.log(getLatestNews);
} catch (error) {
// handle error
alert(error.message);
}
}, 5000);
};
Data returned when logged from console:
Array [
Object {
"category_id": "8",
"content": "Hi",
"created_at": "2020-11-12T12:43:03.000000Z",
"featured_image": "splash-background_1605184983.jpg",
"id": 19,
"news_url": "doerlife.com",
"title": "I m good how about you",
"updated_at": "2020-11-12T12:43:03.000000Z",
}....]
I now save the data into a state array
const [getLatestNews, setLatestNews] = useState([]);
Here is my swipper(some code ommited - not necessary)
<Swiper
ref={useSwiper}
//cards={categoryID(docs, "2")}
cards={getLatestNews}
cardIndex={0}
backgroundColor="transparent"
stackSize={2}
showSecondCard
cardHorizontalMargin={0}
animateCardOpacity
disableBottomSwipe
renderCard={(card) => <Card card={card} />}
.....
When I try to access any data in the array from my Card reusable component, e.g card.featured_image
I WILL GET THIS ERROR - TypeError: undefined is not an object (evaluating 'card.featured_image').
PLEASE CAN SOMEONE HELP ME.
//Card reusable component for deck swipper
import React from 'react'
import { View, Text, Image, ImageSourcePropType } from 'react-native'
import styles from './Card.styles'
const Card = ({ card }) => (
<View activeOpacity={1} style={styles.card}>
<Image
style={styles.image}
source={card.featured_image}
resizeMode="cover"
/>
<View style={styles.photoDescriptionContainer}>
<Text style={styles.title}>{`${card.title}`}</Text>
<Text style={styles.content}>{`${card.content}`}</Text>
<Text style={styles.details}>
Swipe Left to read news in details
</Text>
</View>
</View>
);
export default Card
I've done something similar to this before so I think I can help a bit. The problem here is that your getLatestNews state has not been updated yet before the cards render. You can fix the problem by having another state called "isDataReturned". Then, have a useEffect that triggers whenever getLatestNews's length changes. If getLatestNews's length is > 0, then you can set isDataReturned to be true and render the deck only when isDataReturned is true.
Here's a code sample that I made:
const [getLatestNews, setLatestNews] = useState([]);
const [dataIsReturned, setDataIsReturned] = useState(false)
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'https://cat-fact.herokuapp.com/facts',
);
setLatestNews(result.data);
};
fetchData();
}, []);
useEffect(() => {
if (getLatestNews.length > 0) {
setDataIsReturned(true)
} else {
setDataIsReturned(false)
}
}, [getLatestNews.length])
if( dataIsReturned === true) {
return (
<View style={styles.container}>
<Swiper
cards={getLatestNews}
renderCard={(card) => {
return (
<View style={styles.card}>
<Text>{card.text}</Text>
</View>
)
}}
onSwiped={(cardIndex) => {console.log(cardIndex)}}
onSwipedAll={() => {console.log('onSwipedAll')}}
cardIndex={0}
backgroundColor={'#4FD0E9'}
stackSize= {3}>
</Swiper>
</View>)
} else {
return(<Text>Loading</Text>)
}
In the renderCard attribute, i changed it from
renderCard={(card) => <Cardz card={card} />}
to
renderCard={(card) => (card && <Cardz card={card} />) || null}
and it worked.

How to use Infinite Scroll with FlatList and React Hook?

I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback

Resources