I am rendering a list of Trips objects inside a FlatList. So I have a screen named Network where I have FlatList which represents each of the trips. My render method:
return (
<View style={styles.viewStyle}>
<FlatList
numColumns={1}
horizontal={false}
data={trips}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item, index }) => (
<View key={index}>
<Trip trip={item} = />
</View>
)}
/>
</View>
);
Inside my Trip component is the trip information. Trip's name AND trip's geo locations. From those geolocations I want to get the trip's city and country. To do so I call expo's Location API inside my useEffect function, for each trip:
let response = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
However, it seems that this function id being called only once for the very last trip, from all trips inside my FlatList. This is how my Trip.js component looks like:
import React, { useState, useEffect } from "react";
import { Text, TouchableOpacity } from "react-native";
import * as Location from "expo-location";
const Trip = ({ trip }) => {
const [city, setCity] = useState(null);
const [country, setCountry] = useState(null);
const { latitude, longitude } = trip;
console.log("trip name: ", trip.placeName);
console.log("latitude: ", latitude);
console.log("longitude: ", longitude);
if (!trip) {
return null;
}
useEffect(() => {
console.log("calling use effect from trip summary: ", trip.placeName);
async function fetchLocationName() {
console.log("calling async function");
let response = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
console.log("response: ", response);
setCity(response[0].city);
setCountry(response[0].country);
}
fetchLocationName();
}, [trip.id]);
return (
<TouchableOpacity style={{ flexDirection: "row", flexWrap: "wrap" }}>
<Text>
<Text style={styles.textStyle}>{trip.placeName} </Text>
<Text style={styles.textStyle}>near </Text>
<Text style={styles.textStyleHighlithed}>{city}, </Text>
<Text style={styles.textStyleHighlithed}>{country} </Text>
</Text>
</TouchableOpacity>
);
};
export default Trip;
I put so many console.logs because I wanted to be sure that I have trip.longitude and trip.latitude which, indeed, I have. What I see printed on the console:
latitude: 126.3936269
longitude: 59.3397108
latitude: 71.34165024
longitude: 129.7406225
calling use effect from trip summary: trip one
calling async function
calling use effect from trip summary: second trip
calling async function
response: Array [
Object {
"city": "some city",
"country": "some country",
...
},
]
And indeed on my screen I see only the very last trip's city and country being shown.
How to make sure that my useEffect function is being called for every single trip, not just the last one?
Your logs show that useEffect is being called twice:
calling use effect from trip summary: trip one
calling async function
calling use effect from trip summary: second trip
calling async function
So it's not the useEffect that's the problem. The issue is that you're never getting a return value from Location.reverseGeocodeAsync for one of your calls.
Looking in the Expo docs for Location, you can see the following warning:
Note: Geocoding is resource consuming and has to be used reasonably. Creating too many requests at a time can result in an error, so they have to be managed properly. It's also discouraged to use geocoding while the app is in the background and its results won't be shown to the user immediately.
In the iOS code for expo-location, the following line gets printed if there are too many calls: Rate limit exceeded - too many requests. If you're seeing that line, you need to make a way to space out these requests.
reverseGeocodeAsync also takes an options argument that allows you to use Google's location service instead ({ useGoogleMaps: true }).
So in summary, here are two things to try. You can rewrite your useEffect to explicitly catch errors in case they're not showing up (removed logs for brevity):
useEffect(() => {
async function fetchLocationName() {
try {
const response = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
setCity(response[0].city);
setCountry(response[0].country);
} catch (error) {
console.error(error);
};
}
fetchLocationName();
}, [trip.id]);
And if you are seeing the rate limit error, you would need to build a request queue that spaces out the calls enough to avoid that.
Or you can try using Google's service, which would be the same except for the line that calls reverseGeocodeAsync:
useEffect(() => {
async function fetchLocationName() {
try {
const response = await Location.reverseGeocodeAsync({
latitude,
longitude,
}, { useGoogleMaps: true });
...
You can write your async function like this: try this
useEffect(async()=>{
console.log("calling use effect from trip summary: ", trip.placeName);
let response = await Location.reverseGeocodeAsync({
latitude,
longitude,
});
console.log("response: ", response);
setCity(response[0].city);
setCountry(response[0].country);
},[trip.id])
useEffect runs after component gets rendered on the screen as an effect for the dependency you put in the array, DO ADD ALL THE CONSOLE LOGS OF THE TRIP COMPONENT
in the question.
console.log("trip name: ", trip.placeName);
-------------------------
console.log("calling use effect from trip summary: ", trip.placeName);
Even if you acheive the functionality you intend to, Calling an api from every item of flatlist is not a good approach as it will reduce performance. A better way would be to call the api for particular set of latitude and longitude on some user interaction event. that would be singular, synchronous and performant.
Related
I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};
The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.
I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.
I'm making my first app, "mark what album you've listened" kind of app. I used iTunes search API and for every album listened, I create a key with AsyncStorage using the ID and for the value, the url to the artwork.
So here is the question: I'm stuck at the last step of the app. I want to display all the artwork of all the albums I've listened. For that, I would like to make a foreach loop that for every element in listened, it would take its URL (now that it only contains URLs), put it in an Image tag, return it and display it... But, can I do that?
For that, I created a state called listened. It takes all the AsyncStorage thanks to this function:
importData = async () => {
try {
const keys = await AsyncStorage.getAllKeys();
const result = await AsyncStorage.multiGet(keys);
console.log(result)
//listened takes all asyncstorage data
this.setState({listened: result.map(req => JSON.stringify(req[1]))});
} catch (error) {
console.error(error)
}
}
Then I made a renderArtwork() function that returns the state when I arrive to the Navigation. For now, it just displays all the URLs:
renderArtwork(){
this.importData();
return(
<Text>{this.state.listened}</Text>
)
}
And the "main":
render() {
return(
<View style={styles.main_container}>
{this.renderArtwork()}
</View>
)
}
Thank you for your help
It better to move the importData() to your componentDidMount which will call and get the data from asyncstorage when the screen is mounted.
As for displaying the images, Lets say that your current array 'listened' has the below format
listened = ['url1','url2'];
renderArtwork() {
this.importData();
return this.state.listened.map((url) => (
<Image
style={{
width: 50,
height: 50,
}}
source={{
uri: url,
}}
/>
));
}
You can simply map and show all the images in your array, Also the JSON.stringify part wont be necessary as its already a string.
I have a component that is supposed to log the longitude/latitude/timestamp at regular intervals.
When the user presses START, the tracking should begin. When the user presser STOP, the tracking should stop.
To implement this, I have built the following (I am a complete beginner to react and JS so this could be entirely the wrong way to do this):
const Tracking = props => {
const [currentLatitude, setCurrentLatitude] = useState(0);
const [currentLongitude, setCurrentLongitude] = useState(0);
const [currentTimestamp, setCurrentTimestamp] = useState(0);
const [buttonTitle, setButtonTitle] = useState('Start');
const [isTracking, setIsTracking] = useState(false);
var getLocationInterval;
function getLocation() {
navigator.geolocation.getCurrentPosition(
position => {
setCurrentLongitude(position.coords.longitude);
setCurrentLatitude(position.coords.latitude);
setCurrentTimestamp(position.timestamp);
},
error => alert(error.message),
{ enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
);
console.log(currentTimestamp, currentLatitude, currentLongitude);
};
function startStop() {
if(!isTracking){
//START
setIsTracking(true);
getLocationInterval = setInterval(getLocation, 500);
setButtonTitle('Stop');
}
else{
//STOP
setIsTracking(false);
clearInterval(getLocationInterval);
setButtonTitle('Start');
}
};
return (
<View style={{width: '100%', height: '100%'}}>
<MapView showsUserLocation style={{flex: 1}} />
<MenuButton title = {buttonTitle} onPress={startStop}/>
</View>
);
}
Expected behaviour: once START is pressed, the button text changes to STOP. And in my console, I begin getting an output every 500ms with the latest lat/long/timestamp. When STOP is pressed, the button text changes to START and the outputs stop.
Actual behaviour: once START is pressed, the button text correctly changes to STOP, but only the initial states (0s) are repeatedly output. When I then press STOP, the next lat/long/timestamp starts being repeatedly output to the console. The 0s are also still being output because the interval doesn't seem to stop.
I'm guessing that I'm just using state completely wrong here. Please can someone help me out?
So, there are a few issues in how you've set it up from a logic standpoint. I believe I've fixed all the issues I saw in your code below.
The reason why only 0 was being printed is because the currentLatitude/currentLongitude/currentTimestamp that was being referenced in the getLocation function had a closure around it once the interval was started where it wouldn't refresh to the new version of the function that was created on each re-render. To address that, I removed the references to those variables from the getLocation function in the changes I made.
The reason why there were still 0s being output is because the getLocationInterval was set to undefined in every render. You would've had to make that either a reference or a state variable in order to get that to stay between re-renders.
I moved the logic from the startStop function into a useEffect hook as that is the proper way to make an effect occur from within a hook component. It also allows all the logic to be combined in one straightforward location that can be extracted to a custom hook pretty easily if you need to use it in other locations.
const Tracking = props => {
const [currentLatitude, setCurrentLatitude] = useState(0);
const [currentLongitude, setCurrentLongitude] = useState(0);
const [currentTimestamp, setCurrentTimestamp] = useState(0);
const [isTracking, setIsTracking] = useState(false);
useEffect(() => {
if (!isTracking) return;
function getLocation() {
navigator.geolocation.getCurrentPosition(
position => {
setCurrentLongitude(position.coords.longitude);
setCurrentLatitude(position.coords.latitude);
setCurrentTimestamp(position.timestamp);
console.log(
position.coords.longitude,
position.coords.latitude,
position.timestamp
);
},
error => alert(error.message),
{ enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
);
}
let getLocationInterval = setInterval(getLocation, 500);
return () => clearInterval(getLocationInterval);
}, [isTracking]);
return (
<View style={{ width: '100%', height: '100%' }}>
<MapView showsUserLocation style={{ flex: 1 }} />
<MenuButton
title={isTracking ? 'Stop' : 'Start'}
onPress={() => {
setIsTracking(!isTracking);
}}
/>
</View>
);
};
Edit: Explanation of the return from useEffect:
The return function in line 27 of the code above: return () => clearInterval(getLocationInterval);
The clearInterval isn't immediately run because it's part of an arrow function declaration. We return that function rather than call it directly. Once we return the function declaration, react now has control of the code again and calls it when it wants to.
In particular, useEffect allows you to return a function that is used for clean up. And it calls that function on component unmount or just before it runs the hook again (i.e. the dependency array changed)
There's a little more information on the official react documentation: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect
Im new in ReactNative and I'm trying to take some data from here https://www.dystans.org/route.json?stops=Hamburg|Berlin
When I try console.log results it return full API response. I dont know why in first results.distance works and return distance, but when I'm trying to do it inside FlatList nothing is returned. Sometimes it works when i want to return only item.distance but can't somethnig like <Text>{item.stops[0].nearByCities[0].city}</Text> nowhere in my code also in console. Im getting error:
undefined is not an object (evaluating 'results.stops[0]')
imports...
const NewOrContinueScreen = ({ navigation }) => {
const [searchApi, results, errorMessage] = useDystans();
console.log(results.distance);
return (
<SafeAreaView forceInset={{ top: "always" }}>
<Text h3 style={styles.text}>
Distance: {results.distance}
</Text>
<Spacer />
<FlatList
extraData={true}
data={results}
renderItem={({ item }) => (
<Text>{item.distance}</Text>
// <Text>{item.stops[0].nearByCities[0].city}</Text>
)}
keyExtractor={item => item.distance}
/>
<Spacer />
</SafeAreaView>
);
};
const styles = StyleSheet.create({});
export default NewOrContinueScreen;
And here is my hook code:
import { useEffect, useState } from "react";
import dystans from "../api/dystans";
export default () => {
const [results, setResults] = useState([]);
const [errorMessage, setErrorMessage] = useState("");
const searchApi = async () => {
try {
const response = await dystans.get("route.json?stops=Hamburg|Berlin", {});
setResults(response.data);
} catch (err) {
setErrorMessage("Something went wrong with useDystans");
}
};
useEffect(() => {
searchApi();
}, []);
return [searchApi, results, errorMessage];
};
As the name implies, FlatList is designed to render a list. Your API endpoint returns a JSON Object, not an Array, so there's nothing for the FlatList to iterate. If you want to show all the stops in the list, try passing in the stops list directly.
<FlatList
data={results.stops}
renderItem={({ item }) => (<Text>{item.nearByCities[0].city}</Text>)}
/>
Some side notes: (1) The extraData parameter is used to indicate if the list should re-render when a variable other than data changes. I don't think you need it here at all, but even if you did, passing in true wouldn't have any effect, you need to pass it the name(s) of the variable(s). (2) The keyExtractor parameter is used to key the rendered items from a field inside of them. The stop objects from the API don't have a member called distance so what you had there won't work. From my quick look at the API response, I didn't see any unique IDs for the stops, so you're probably better off letting React key them from the index automatically.
I have populated a FlatList with data fetched from Google's firebase backend. The implementation is rather standard, here's a stripped down version:
export default class Day extends Component {
state = { data : [], today: false }
componentWillMount = async () => {
const { today } = this.state;
const { calendarDb } = this.props
await calendarDb.onNewAgenda({
day : today
, then: this.parseOnListed
})
}
parseOnListed = blob => {
const { data } = this.state;
data.push(blob)
this.setState({ data: data })
}
renderItem = ({ item }) =>
<Hour data = {item}/>
render = () =>
<FlatList
data = {this.state.data}
renderItem = {this.renderItem}
keyExtractor = {item => item.ID}
/>
}
The issue is that every time a new blob is pushed into data, the <Image/> component in <Hour data={item}/> flickers. This makes the list a no-go in terms of user experience. What gives? <Hour/> is standard as well, and more or less look like this:
const Hour = ({ data }) =>
<View>
<Image source={{uri:data.uri}}/>
<Text> {data.name} </Text>
</View>
The content of <Text> does not flicker, only the image from <Image .../>
Check whether keyExtractor is getting unique ID or not.
The flat list is re-rendering on state update and images are downloaded again. Because, each row is not uniquely identified as said in comments by #Guruparan Giritharan.
I found another reason that triggers this issue, of the FlatList flikering on React native. In my case, it happened every time I updated/changed the state of any function component. So, for instance, I was keeping the fetch results (data) and the next-page-id (for the next paginated fetch) in two separate function components:
const [data, setData] = useState([]);
const [pageId, setPageId] = useState(null);
Hence, every time would capture the results of my fetch, I would first set the data update and then the page id. It was the page id update what was causing the flicker.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setData(result.Data);
setPageId(result.pageId);
});
}
The fix was just to put the state data together so there is a single update instead. Then react is happy and doesn't flicker when adding new items to the list.
const onEndReachedFetch = async () ={
fetch(pageId).then(result => {
setResult(result);
});
}
Beware of any side states that you may be updating in the background, as they may also cause the flickering if they are triggered by anything on the FlatList.