I have a horizontal scrolling flatlist in my app. But onViewableItemsChanged keeps throwing an error. 'Changing onViewableItemsChanged on the fly is not supported.' I write the codes below. Please help me!
const flatListRef = useRef(null);
const [currentSectionIndex, setCurrentSectionIndex] = useState(0);
const onButtonPress = useCallback(() => {
if (currentSectionIndex === searchedData?.length - 1) {
// What you want to do at the end of the list.
} else if (flatListRef.current) {
// Go to the next item
flatListRef.current.scrollToIndex({
index: currentSectionIndex + 1,
});
setCurrentSectionIndex(currentSectionIndex + 1);
}
}, [currentSectionIndex, searchedData?.length]);
const onLeftButtonPress = useCallback(() => {
flatListRef.current.scrollToIndex({
index: currentSectionIndex - 1,
});
setCurrentSectionIndex(currentSectionIndex - 1);
}, [currentSectionIndex, searchedData?.length]);
const viewabilityConfig = useRef({
itemVisiblePercentThreshold: 100,
waitForInteraction: true,
minimumViewTime: 5,
})
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
if (viewableItems.length >= 1) {
setCurrentSectionIndex(viewableItems[0].index);
}
}, [])
<FlatList
ref={flatListRef}
horizontal
pagingEnabled // cause snapping to items
data={data}
keyExtractor={(item,index)=>index.toString()}
renderItem={renderItem}
viewabilityConfig={viewabilityConfig.current}
onViewableItemsChanged={onViewableItemsChanged}
/>
{currentSectionIndex!==searchedData?.length-1&&!enteredString?<>
<View> //splinter
<TouchableOpacity onPress={()=>onButtonPress()}>
<Entypo name={'chevron-thin-right'} size={38} color={'#fff'}/>
</TouchableOpacity>
</View>
</>:null}
Related
I want to plot my data in React-Native. I am receiving data from BLE. But I can't see the real time graph on the screen. What could be the reason? My brain stopped, so I couldn't figure out what the problem was.
Note : My data is,
hr
This is BLE characteristic value.
This is my component.js,
const width = Dimensions.get('window').width;
const height = Math.floor((Dimensions.get('window').height - 150) / 3);
let counter = 0;
const slotsPerWidth = 100;
const initialState = {
flow: hr,
isLoading: false,
};
class ChartScreen extends Component {
state = {
chartData: { ...initialState },
};
static getDerivedStateFromProps(state) {
counter++;
return {
chartData: {
flow: [hr],
},
};
}
render() {
const { flow} = this.state.chartData;
return this.state.isLoading
? <ScreenContainer>
<View style={styles.main}>
<View>
<ActivityIndicator></ActivityIndicator>
</View>
<Chart
key="flow"
data={flow}
maxValue={1900}
minValue={1750}
slotsPerWidth={slotsPerWidth}
width={width}
height={height}
marginBottom={20}
lineColor="rgba(95, 92, 1, 1)"
lineThickness={2}
chartBackground="#17204d"
horizontalGridLinesCount={5}
gridColor="rgba(65, 95, 93, .4)"
gridThickness={1}
unit="ml"
axisTooClose={10}
labelsColor="rgba(255, 255, 255, 0.8)"
labelsFontSize={12}
marginLeft={60}
labelsMarginLeft={15}
/>
</View>
</ScreenContainer>:
null
}
}
const styles = StyleSheet.create({
main: {
flex: 1,
},
});
export default ChartScreen;
This is my device screen.tsx,
const DeviceScreen = ({
route,
navigation,
}: StackScreenProps<RootStackParamList, 'Device'>) => {
// get the device object which was given through navigation params
const { device } = route.params;
const [isConnected, setIsConnected] = useState(false);
const [services, setServices] = useState<Service[]>([]);
// handle the device disconnection
const disconnectDevice = useCallback(async () => {
navigation.goBack();
const isDeviceConnected = await device.isConnected();
if (isDeviceConnected) {
await device.cancelConnection();
navigation.navigate('Home');
}
}, [device, navigation]);
useEffect(() => {
const getDeviceInformations = async () => {
// connect to the device
const connectedDevice = await device.connect();
setIsConnected(true);
// discover all device services and characteristics
const allServicesAndCharacteristics = await connectedDevice.discoverAllServicesAndCharacteristics();
// get the services only
const discoveredServices = await allServicesAndCharacteristics.services();
setServices(discoveredServices);
PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Permission Localisation Bluetooth',
message: 'Requirement for Bluetooth',
buttonNeutral: 'Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
};
getDeviceInformations();
device.onDisconnected(() => {
navigation.navigate('Home');
});
// give a callback to the useEffect to disconnect the device when we will leave the device screen
return () => {
disconnectDevice();
};
}, [device, disconnectDevice, navigation]);
return (
<ScrollView contentContainerStyle={styles.container}>
<TouchableOpacity style={styles.button} onPress={disconnectDevice}>
<Text style={{fontFamily:"SairaExtraCondensedThin",textAlign:"center",fontSize:15,color:"white"}}>Antrenmanı Sonlandır</Text>
</TouchableOpacity>
<View>
<View style={styles.header} >
<Text>{`Name : ${device.name}`}</Text>
<Text>{`Is connected : ${isConnected}`}</Text>
</View>
{services &&
services.map((service) => <ServiceCard service={service} />)}
</View>
<View>
<ChartScreen chartdata />
</View>
</ScrollView>
);
};
Well, at least, in component.js the isLoading state is never set to true and in screen.tsx you are non passing any data since <ChartScreen chartdata /> means <ChartScreen chartdata={true} />, so it is a boolean property. Then, in getDerivedStateFromProps you are increasing counter that is not part of the state, so, by itself, it will not trigger a new render. Check also that the value of hr is set in the proper place, looks strange to me to see it not as a part of the state too, or as a property. It needs some investigation. Also, the useEffect dependencies are objects so check this out, but consider to refactor the useEffect part, react useEffect comparing objects
Note
Seems to me that there are some misconceptions about React here cousing most of the issues.
This may not be the actual and full answer but I decided to provide my input as a starting point for further investigation and impovements.
Hi guys I am currently facing a problem where in I need to update the data inside of a pop up
here's my code
useEffect(() => {
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
accepted,id,name,rating,services,userDestinationLat,userDestinationLng,userOriginLat,userOriginLng
} = doc.data();
list.push({ accepted, id, name, rating, services, userDestinationLat, userDestinationLng, userOriginLat, userOriginLng });
});
setUserBookingData(list);
});
}, []);
useEffect(() => {
// use this just to get userbookingData
console.log(userBookingData);
},[userBookingData]);
const [newOrder, setNewOrder] = useState({
id: '1',
service: userBookingData.services,
originLatitude : originalPos.latitude,
originLongitude: originalPos.longitude,
destinationLatitude: 45.24953,
destinationLongitude: -76.360733,
user:{
rating: userBookingData.rating,
name: userBookingData.name,
}
});
now here on my return view
return(
{ !userBookingData.length ? (<NewOrderPopUp
newOrder={newOrder}
onDecline={onDecline}
duration={2}
distance={0.5}
onAccept={() => onAccept(newOrder)}
/>) : (
<View></View>
)
}
);
here's the popup component it's in another page
const NewOrderPopUp = ({newOrder, onAccept, onDecline, duration, distance}) => {
return (
<View style={styles.root}>
<Pressable onPress={onDecline} style={styles.declineButton}>
<Text style={styles.declineText}>Decline</Text>
</Pressable>
<Pressable onPress={onAccept} style={styles.popupContainer}>
<View style = {styles.row}>
<Text style = {styles.service}>{newOrder.service}</Text>
{/* <Image source={{}}/> */}
<View style={styles.userBg}>
<FontAwesome name={"user"} color={"white"} size={35}/>
</View>
<Text style = {styles.service}>
<AntDesign name={"star"} size={16}/>
{newOrder.user.rating}
</Text>
</View>
<Text style = {styles.minutes}>{duration} mins</Text>
<Text style = {styles.distance}>{distance} KM</Text>
</Pressable>
</View>
);
};
it won't update the data / show the pop up wen there's a data
The expectation output is it should be able to pop up the NewOrderPopUp screen when there's a data.
here's the data from the userBookingData
I need to display it here
The fact that you are filling userBookingData in first useEffect does not allow you to write an useState getting value from userBookingData itself.
React doesn't work like that.
Not only but if I understand correctly, userBookingData is an array so write userBookingData.services returns null.
Lets suppose that you want to have in newOrder the first element that comes from userBookingData. In this case, you should write something like:
const [newOrder, setNewOrder] = useState({});
useEffect(() => {
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const {
accepted,id,name,rating,services,userDestinationLat,userDestinationLng,userOriginLat,userOriginLng
} = doc.data();
list.push({ accepted, id, name, rating, services, userDestinationLat, userDestinationLng, userOriginLat, userOriginLng });
});
setUserBookingData(list);
// here set newOrder
let resultObj = {};
resultObj.id = 1;
resultObj.service = list[0].services;
...
setNewOrder(resultObj);
});
}, []);
I'm getting the data from the database and show it in a FlatList. Whenever I add or remove something from the data the data isn't showing correctly in the FlatList.
Whenever I remove something it shows an empty list.
Whenever I add something it only shows the newly added data - nothing else.
I'm using firebase realtime database and use the data I get as follows:
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
// setWords([...data]) doesn't work either.
}
})
My Flatlist looks like this:
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
extraData={words}
/>
When I console.log() the data I always get the data I want to show but the FlatList just won't show it correctly.
It also doesn't work when I use the spread-operator and/or extraData.
Because someone asked for it here is the entire file (I left out the styling and the imports)
const EditList = ({ editKey }) => {
const [wordlist, setWordlist] = useState(0);
const [refresh, setRefresh] = useState(false);
const [words, setWords] = useState([]);
const [wordLoading, setWordLoading] = useState({ loading: false });
const [loading, setIsLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [word, setWord] = useState('');
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
const data = [];
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords([...data]);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
}
}, [editKey])
const onAdd = () => {
setBtnLoading(true);
firebase.database().ref(`/wordlists/${editKey}/words`).push({ word })
.then(() => {
setBtnLoading(false);
setWord('');
setRefresh(!refresh);
})
}
const onDelete = (key) => {
setWordLoading({ key, loading: true });
firebase.database().ref(`/wordlists/${editKey}/words/${key}`).remove().then(() => {
setWordLoading({ loading: false });
setRefresh(!refresh);
});
}
const renderItem = ({ item }) => (
<ItemWrapper>
<ItemWord>{ item.word }</ItemWord>
<DeleteView onPress={() => onDelete(item.key)}>
{ wordLoading.loading && wordLoading.key === item.key ?
<ActivityIndicator size="small" /> :
<DIcon name="trash-2" size={24} />
}
</DeleteView>
</ItemWrapper>
)
const createData = (words) => {
const data = [];
if (typeof words !== 'undefined') {
Object.keys(words).forEach(key => {
const obj = { key, word: words[key].word };
data.push(obj);
})
}
console.log(data, 'DATADATADATA');
return data;
}
if (editKey === 0) {
return (
<NokeyWrapper>
<NoKeyText>No list selected...</NoKeyText>
</NokeyWrapper>
)
}
if (loading) {
return (
<NokeyWrapper>
<ActivityIndicator size="large" />
</NokeyWrapper>
)
}
return (
<Wrapper
behavior={Platform.OS == "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === 'ios' && 180}
>
<WordListName>{wordlist.listName}</WordListName>
<FlatListWrapper>
<FlatList
data={words}
renderItem={renderItem}
keyExtractor={item => item.key}
//extraData={refresh}
extraData={words}
/>
</FlatListWrapper>
<AddWordWrapper>
<SInput value={word} onChangeText={(text) => setWord(text)} />
<Button onPress={() => onAdd()} loading={btnLoading}>
<Feather name="plus" size={24} color="black" />
</Button>
</AddWordWrapper>
</Wrapper>
)
};
export default EditList;
u need to useRef for this instance because the new 'words' is not inside the .on('value') call.
const [words, _setWords] = useState([]);
const wordRef = useRef(words)
//function to update both wordRef and words state
const setWords = (word) => {
wordRef = word
_setWords(word)
}
useEffect(() => {
if (editKey !== 0) {
setIsLoading(true);
let data = wordRef //create a temp data variable
firebase.database().ref(`/wordlists/${editKey}`).on('value', snap => {
if (snap.val() !== null) {
setIsLoading(false);
setWordlist({...snap.val()});
const val = snap.val().words;
Object.keys(val).forEach(key => {
data.push({ key, word: val[key].word });
})
setWords(data);
setRefresh(!refresh);
console.log(data, 'DATA');
}
})
return () => firebase.database().ref(`/wordlists/${editKey}`).off('value') // <-- need to turn it off.
}
}, [editKey, wordRef])
You probably need to change setRefresh etc with the same method if they are not refreshing.
After a lot more tries I found out the problem was somewhere else. Somehow using 'flex: 1' on my in my renderItem() was causing this issue. I actually found this issue also on github: GitHub React Native issues
So after removing 'flex: 1' from the element everything was showing as expected.
// before
const renderItem = ({ item }) => (
<ItemWrapper style={{ flex: 1, flexDirection: row }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
// after
const renderItem = ({ item }) => (
<ItemWrapper style={{ width: '100%' }}>
<ItemWord>{ item.word }</ItemWord>
</ItemWrapper>
)
I'm doing the notification page of my react native app. It has infinite scroll and "pull to refresh" options. Entering to the page it works, and it works also pulling to refresh.
The problem occurs when I scroll down because it seems it calls server to fetch new notifications but it doesn't concatenate to the array.
import React, { useState, useEffect, useCallback, Component } from "react";
import {
View,
Text,
FlatList,
Button,
Platform,
ActivityIndicator,
StyleSheet,
ScrollView,
RefreshControl,
SafeAreaView,
} from "react-native";
import { useSelector, useDispatch } from "react-redux";
import i18n from "i18n-js";
import Colors from "../../constants/Colors";
import { getNotificationList } from "../../utils/NotificationsUtils";
import Card from "../../components/UI/Card";
const NotificationsScreen = (props) => {
const [refreshing, setRefreshing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(0);
const [notifications, setNotifications] = useState([]);
const [error, setError] = useState();
const dispatch = useDispatch();
const onRefresh = useCallback(async () => {
setRefreshing(true);
setNotifications([]);
setPage(0);
console.log("-- Refreshing --");
getNotifications().then(() => {
setRefreshing(false);
});
}, [dispatch, setRefreshing]);
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
console.log(
"FETCH MORE from page " + newPage + " on array of " + notifications.length
);
getNotifications().then(() => {
setIsLoading(false);
});
}, [dispatch, getNotifications]);
const getNotifications = useCallback(async () => {
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
console.log(
"Setting " +
retrievedNotifications.response.notifications.length +
" new notifications on an already existing array of " +
notifications.length +
" elements"
);
let updatedNews = notifications.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
);
setNotifications(updatedNews);
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}, [dispatch, setIsLoading, setNotifications, setError]);
useEffect(() => {
setIsLoading(true);
getNotifications(page).then(() => {
setIsLoading(false);
});
}, [dispatch, getNotifications]);
return (
<View>
{error ? (
<View style={styles.centered}>
<Text>Error</Text>
</View>
) : refreshing ? (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colors.primary} />
</View>
) : !notifications || !notifications.length ? (
<View style={styles.centered}>
<Text>No data found</Text>
</View>
) : (
<FlatList
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
data={notifications}
keyExtractor={(notification) => notification.notificationQueueId}
onEndReached={fetchMoreNotifications}
onEndReachedThreshold={0.5}
initialNumToRender={4}
renderItem={(itemData) => (
<View
style={{
marginTop: 10,
height: 150,
width: "100%",
}}
>
<Card style={{ height: 150, backgroundColor: "white" }}>
<Text style={{ fontSize: 16, color: Colors.black }}>
{itemData.item.text}
</Text>
</Card>
</View>
)}
/>
)}
</View>
);
};
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
});
export default NotificationsScreen;
If I scroll to end it triggers 'fetchMoreNotifications' function and I get this in the console:
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
FETCH MORE from page 7 on an array of 0
Get from page 0
Setting 7 new notifications on an already existing array of 0 elements
...and so on
As you can see it says 'existing array of 0 elements' even if previously I saved notifications. Maybe it has some issue with useCallback's dependency?
Issue :
There are 2 main issues, one with page and second with notifications, due to useCallback and dependencies, useCallback function will always point to the old values which are not in dependencies until one of the dependencies for updated.
1) The solution to page issue :
Pass newPage as param to getNotifications, due to async behavior of setPage it will not get updated directly
And on the second time, to get the updated value of page you can pass page as a dependency.
2) The solution to the notification issue :
Update the notification directly from its prev state value with setState(prevState => newState).
Solution :
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
console.log(
"FETCH MORE from page " + newPage + " on array of " + notifications.length
);
getNotifications(newPage).then(() => { // <---- Pass as param
setIsLoading(false);
});
}, [page]); // <---- Add page as dependency
const getNotifications = useCallback(
async page => { // <---- Get page as a param
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
setNotifications(prevNotification => prevNotification.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
)); // <---- Setting up state directly from previous value, instead of getting it from clone version of use callback
} catch (err) {
console.log(err);
setError(err.message);
}
setIsLoading(false);
},
[setIsLoading, setNotifications, setError]
);
WORKING DEMO :
Check the console log for updated page value and notification will be rendered on Html it self
NOTE : Removed some of your code just to improve code readability and
debug the issue
The problem is really simple. The getNotifications function is created using useCallback and hasn't used notifications as a dependency. Now when notifications updates, the getNotications function is still referring to the old notifications values due to closure.
Also note that you call getNotifications on fetchMoreNotifications immediately after setting page state but page state too is bound by closure and will not update in the same re-render
The solution here is to use the function approach to setNotifications and use useEffect to trigge4r getNotification on page change
const fetchMoreNotifications = useCallback(async () => {
const newPage = page + 7;
setPage(newPage);
}, [dispatch, getNotifications]);
useEffect(() => {
setIsLoading(true);
getNotifications(page).then(() => {
setIsLoading(false);
});
}, [dispatch, page, getNotifications]);
const getNotifications = useCallback(async () => {
setError(null);
setIsLoading(true);
try {
console.log("Get from page " + page);
// let fromRecord = (page - 1) * 7;
const retrievedNotifications = await getNotificationList(
page,
7,
true,
false
);
setNotifications(prevNotification => prevNotification.concat(
retrievedNotifications &&
retrievedNotifications.response &&
retrievedNotifications.response.notifications
));
} catch (err) {
setError(err.message);
}
setIsLoading(false);
}, [dispatch, setIsLoading, setNotifications, setError]);
I want to scroll FlatList to the certain index when screen is launched and data for the list is retrieved from the server.
I have a problem of using ref inside useMemo(). I'm getting an error:
Cannot read property current of undefined.
How to fix this error? Does my approach correct?
Here is what I'm doing:
const WeeklyMeetings = props => {
const [meetings, setMeetings] = useState(null)
useEffect(() => {
AllMeeting() // getting data from the server
}, [])
const getResult = useMemo(() => {
flatListRef.current.scrollToIndex({index: 15, animated: true })
}, [meetings]);
const flatListRef = useRef();
const AllMeeting = async (id) => {
setLoading(true)
try {
const meetings = await meeting.allMeetingsAsc(id)
setMeetings(meetings)
} catch (error) {
console.log("error ", error)
}
}
return (
<View style={styles.rootContainer}>
<FlatList
ref={flatListRef}
style={styles.list}
data={meetings}
renderItem={renderItem}
onScrollToIndexFailed={()=>{}}
/>
</View>
)
}
The ref needs to be defined before using it.
Also since you want to just scroll to index when you receive meeting value, you can make use of useEffect hook.
Also note that you only want to scrollToIndex once value meetings is available and hence you can skip the initial call to useEffect by keeping track of initialRender
const WeeklyMeetings = props => {
const [meetings, setMeetings] = useState(null)
useEffect(() => {
const AllMeeting = async (id) => {
setLoading(true)
try {
const meetings = await meeting.allMeetingsAsc(id)
setMeetings(meetings)
} catch (error) {
console.log("error ", error)
}
}
AllMeeting();
}, [])
const flatListRef = useRef();
const initialRender = useRef(true)
useEffect(() => {
if(!initialRender.current) {
flatListRef.current.scrollToIndex({index: 15, animated: true })
} else {
initialRender.current = false;
}
}, [meetings])
return (
<View style={styles.rootContainer}>
<FlatList
ref={flatListRef}
style={styles.list}
data={meetings}
renderItem={renderItem}
getItemLayout={(data, index) => (
{length: 50, offset: 50 * index, index}
)}
/>
</View>
)
}
According to Documentation
You would need to implement a getItemLayout function for FlatList
since scrollToIndex Cannot scroll to locations outside the render
window without specifying the getItemLayout prop.