I am writing react-native app and I am trying to get user location address upon loading splash screen. It works but I am not sure whether I am doing things in a right way. Should I need to put verifyPermissions() and fetchLocationUserAddress() in useEffect hook? How would you approach if you have the task to refactor this code? The code is below. This is the first loaded screen in my app.
export default function SplashScreen({navigation}) {
const [locationPermissionInformation, requestPermission] = useForegroundPermissions()
const dispatch = useDispatch()
const verifyPermissions = async () => {
if (locationPermissionInformation.status === PermissionStatus.UNDETERMINED) {
const permissions = await requestPermission()
return permissions.granted
}
if (locationPermissionInformation.status === PermissionStatus.DENIED) {
Alert.alert('You need to grant access to use this application!')
return false;
}
return true;
}
const locationHandler = async () => {
const awaitPermission = await verifyPermissions();
if (!awaitPermission) {
return;
}
const locationUser = await getCurrentPositionAsync();
console.log(locationUser)
let lat = locationUser.coords.latitude
let lon = locationUser.coords.longitude
fetchLocationUserAddress(lat,lon)
dispatch(setLocation({lat,lon}))
}
const fetchLocationUserAddress = (lat,long) => {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?address=${lat},${long}&key=AIzaSyCbJ4Xt40x94OBFcSQat5BI00dDf8ZwF4s`)
.then((response) => response.json())
.then((json) => {
console.log(json)
dispatch(setFormattedLocation(json.results[0].formatted_address))
})
.catch((error) => console.error(error))
}
const onSplashScreenLoadedHandler = async (e) => {
await locationHandler()
// e.preventDefault();
setTimeout(() => {
navigation.navigate("PrivacyPolicy");
}, 2000);
}
return (
<View style={styles.container}>
<Image
style={styles.flag}
source={require("../../media/6328868-removebg-preview1.png")}
/>
<Image
onLoadEnd={onSplashScreenLoadedHandler}
style={styles.logo}
source={require("../../media/road-runner-logo-vector-2440181.png")}
/>
</View>
);
I am new in react native world and want to get some advice from programmers with more experience.
Related
Question:
I am developing a small app that is a memory game of Formula One Drivers to practice React. It makes a call to an API to get the driver info then I have to make a second API call to Wikipedia to get the driver images. When I submit the year and click the button it will only load half the information Image 1 & getDrivers function. When I click the button again it will load the images Image 2 & getDriversImgs function / retrievingImgUrl.
I believe I am encountering a GOTCHA or doing something fundamentally wrong. I am not sure in my setDrivers call in the retrievingImgUrl() function if it isn't updating because it is a reference to an array even though I use map and it should be returning a new array?
Or is this something where I need to use useEffect or useCallback to have the code rerender in one go?
Any advice on how to fix the bug and if you could point me in a direction to possibly clean up these fetch calls or would you consider this clean code (like conceptually chaining fetch calls together in smaller functions or should I make it one big function)?
import { Fragment, useState, useEffect } from "react";
// Components
import Header from "./components/header/Header";
import CardList from "./components/main/CardList";
import Modal from "./components/UI/Modal";
// CSS
import classes from "./App.module.css";
function App() {
const [drivers, setDrivers] = useState([]);
const getDrivers = async (year) => {
const response = await fetch(
"https://ergast.com/api/f1/" + year + "/drivers.json"
);
const data = await response.json();
let driverInfo = [];
data.MRData.DriverTable.Drivers.map((driver) => {
driverInfo.push({
id: driver.code,
firstName: driver.givenName,
lastName: driver.familyName,
wikipedia: driver.url,
image: null,
});
});
setDrivers(driverInfo);
getDriversImgs();
};
async function getDriversImgs() {
console.log(drivers);
const responses = await Promise.all(
drivers.map((driver) => {
let wikiPageName = driver.wikipedia.split("/").slice(-1).toString();
let wiki_url =
"https://en.wikipedia.org/w/api.php?origin=*&action=query&titles=" +
wikiPageName +
"&prop=pageimages&format=json&pithumbsize=500";
return fetch(wiki_url);
})
);
const urls = await Promise.all(responses.map((r) => r.json())).then(
(json) => retrievingImgUrl(json)
);
setDrivers((prev) => {
return prev.map((item, idx) => {
return { ...item, image: urls[idx] };
});
});
}
const retrievingImgUrl = async (data) => {
console.log(data);
const strippingData = data.map((d) => {
return d.query.pages;
});
const urls = strippingData.map((d) => {
const k = Object.keys(d)[0];
try {
return d[k].thumbnail.source;
} catch {
return null;
}
});
return urls;
};
return (
<Fragment>
<Header getDrivers={getDrivers} />
<CardList drivers={drivers} />
</Fragment>
);
}
export default App;
Image 1 (clicked button once):
Image 2 (clicked button twice):
Object20Object error:
const Header = (props) => {
const driverYear = useRef();
const driverYearHandler = (e) => {
e.preventDefault();
console.log(driverYear);
const year = driverYear.current.value;
console.log(typeof year);
props.getDrivers(year.toString());
};
return (
<header className={classes.header}>
<Title />
<form onSubmit={driverYearHandler}>
{/* <label htmlFor="year">Enter Year:</label> */}
<input
type="text"
id="year"
ref={driverYear}
placeholder="Enter Year:"
/>
<button onClick={props.getDrivers}>Get Drivers</button>
</form>
</header>
);
};
export default Header;
Console Error:
UPDATED FETCH CALL
const getDrivers = async (year) => {
console.log("Running more than once??");
const url = "https://ergast.com/api/f1/" + year + "/drivers.json";
const response = await fetch(url);
const data = await response.json();
let driverInfo = [];
data.MRData.DriverTable.Drivers.map((driver) => {
driverInfo.push({
id: driver.code,
firstName: driver.givenName,
lastName: driver.familyName,
wikipedia: driver.url,
image: null,
});
});
getDriversImgs(driverInfo).then((data) => setDrivers(data));
console.log("Here is driver info", driverInfo);
};
const getDriversImgs = async (driverInfo) => {
const responses = await Promise.all(
driverInfo.map((driver) => {
let wikiPageName = driver.wikipedia.split("/").slice(-1).toString();
let wiki_url =
"https://en.wikipedia.org/w/api.php?origin=*&action=query&titles=" +
wikiPageName +
"&prop=pageimages&format=json&pithumbsize=500";
return fetch(wiki_url);
})
);
const urls = await Promise.all(responses.map((r) => r.json())).then(
(json) => retrievingImgUrl(json)
);
return driverInfo.map((item, idx) => {
return { ...item, image: urls[idx] };
});
};
const retrievingImgUrl = async (data) => {
const strippingData = data.map((d) => {
return d.query.pages;
});
const urls = strippingData.map((d) => {
const k = Object.keys(d)[0];
try {
return d[k].thumbnail.source;
} catch {
return null;
}
});
return urls;
};
This is likely happening because of a small misunderstanding with setState. You are calling getDriversImgs() just after setDrivers() is called, but any set state function is asynchronous. It is likely not done setting before you look for the driver's image.
The simplest solution in my opinion will be to not setDrivers until you've correlated an image to each driver. You already have all of your driverInfo in an array, so iterating through that array and finding the image for the driver should be quite straightforward.
After you've created a driverInfo array that includes the driver's image, then you can use setDrivers which will render it to the DOM.
I have a component that fetches the data properly but I want to encapsulate it in a helper. I've tried many things but I'm stuck.
This is the component that works:
export const Carousel = () => {
const [ lotteries, setLotteries ] = useState({});
const [ isLoading, setisLoading ] = useState(true);
useEffect(() => {
async function fetchAPI() {
const url = 'https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
response = await response.json()
setLotteries(response)
setisLoading(false)
}
fetchAPI()
}, [])
return (
<>
{
isLoading ? (
<span>loading</span>
) : (
<Slider >
{
Object.keys(lotteries).map((lottery, idx) => {
return (
<Slide
key={ idx }
title={ lottery }
prize={ lotteries[lottery].next.jackpot }
day={ lotteries[lottery].next.date.day }
/>
)
})
}
</Slider>
)}
</>
);}
And this is the last thing I've tried so far. This is the component without the fetch
export const Carousel = () => {
const [ lotteries, setLotteries ] = useState({});
const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => {
getLotteries()
setLotteries(response)
setIsLoading(false)
}, [])
And this is where I tried to encapsulate the fetching.
export const getLotteries = async() => {
const url = 'https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
response = await response.json()
return response;
}
I'm a bit new to React, so any help would be much appreciated. Many thanks.
To get the fetched data from getLotteries helper you have to return a promise
export const getLotteries = async() => {
const url = 'https://protected-sea-
30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
return response.json()
}
and call it as async/await
useEffect(async() => {
let response= await getLotteries()
setLotteries(response)
setIsLoading(false)
}, [])
If you want to separate the logic for requesting a URL into another helper function, you can create a custom hook.
// customHook.js
import { useEffect, useState } from 'react';
export function useLotteries() {
const [lotteries, setLotteries] = useState(null);
useEffect(() => {
fetch('https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;')
.then(response => response.json())
.then(json => setLotteries(json));
}, []);
return lotteries;
}
// Carousel.js
import { useLotteries } from "./customHook.js";
export const Carousel = () => {
const lotteries = useLotteries();
if (lotteries) {
return; /* Your JSX here! (`lotteries` is now contains all the request responses) */
} else {
return <Loader />; // Or just null if you don't want to show a loading indicator when your data hasn't been received yet.
}
};
Hello I am new to react native and cant figure out this problem please help me I am working in a project .In my project I want all the tabs and tab content to be autogenerated dynamically from my firebase database
I need that all these nodes (Living room, kitchen, bedroom, etc.) to be the names of the tab and all tabs should show its own content directly from firebase like Living room tab will show app_1 and app_2 similarly Kitchen and bedroom will also autogenerate these directly from database
Like this image:
const HorScrollView = () => {
const [homeId, setHomeId] = useState(0);
const [roomList, setRoomList] =useState(["Loading Rooms...."]);
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve,reject) => {
database().ref(`/USERS/${user.uid}/home_id`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
};
const roomListProvider = ()=>{
return new Promise((resolve,reject) => {
database().ref(`/HOMES/${homeId}/rooms`).once('value').then(snapshot => {
resolve(snapshot.val());
});
});
}
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
return (
<View style={styles.scrollViewContainer} >
<ScrollView horizontal>
{roomList.map((roomlist) => (
<Pressable key={roomlist}>
<Text style={styles.scrollViewText} >{roomlist}
</Text>
</Pressable>
))}
</ScrollView>
</View>
);
};
There's a UI library called antd which can help you do this. Use a <Table/> tag for this, then as props pass the value of the columns attribute to be the titles that are coming through, and then the dataSource attribute will be the data under those specific columns. Read the documentation for more.
While you can use the <Tabs> component from antd to achieve what you want, your current code has some bugs regarding how it handles user state and the asynchronous calls.
Taking a look at these lines:
const homeidProvider = () => {
const user = auth().currentUser;
return new Promise((resolve, reject) => {
database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => {
resolve(snapshot.val());
});
});
};
Here there are two main problems:
You make use of auth().currentUser but this isn't guaranteed to contain the user object that you expect as it may still be resolving with the server (where it will be null) or the user may be signed out (also null).
You incorrectly chain the promise by wrapping it in a Promise constructor (known as the Promise constructor anti-pattern) where the errors of the original promise will never reach the reject handler leading to crashes.
To fix the user state problem, you should make use of onAuthStateChanged and look out for when the user signs in/out/etc.
function useCurrentUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
// returns [firebase.auth.User | null, boolean]
return [user || null, userLoading];
}
// in your component
const [user, userLoading] = useCurrentUser();
To fix the PCAPs, you'd use:
const homeidProvider = () => {
return database()
.ref(`/USERS/${user.uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const roomListProvider = () => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
Because these functions don't depend on state changes, you should place them outside your component and pass the relevant arguments into them.
Next, these lines should be inside a useEffect call where error handling and unmounting the component should be handled as appropriate:
const callMe = async () => {
let home_id = await homeidProvider();
setHomeId(home_id);
let roomdata = await roomListProvider();
setRoomList((Object.keys(roomdata)).reverse());
}
callMe();
should be swapped out with:
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
return;
}
let disposed = false;
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
You should also track the status of your component:
Status
Meaning
"loading"
data is loading
"error"
something went wrong
"signed-out"
no user logged in
"ready"
data is ready for display
Rolling this together:
import { Tabs, Spin, Alert, Card } from 'antd';
const { TabPane } = Tabs;
const { Meta } = Card;
function useUser() {
const [user, setUser] = useState(() => auth().currentUser || undefined);
const userLoading = user === undefined;
useEffect(() => auth().onAuthStateChanged(setUser), []);
return [user || null, userLoading];
}
const getUserHomeId = (uid) => {
return database()
.ref(`/USERS/${uid}/home_id`)
.once('value')
.then(snapshot => snapshot.val());
};
const getHomeRoomData = (homeId) => {
return database()
.ref(`/HOMES/${homeId}/rooms`)
.once('value')
.then(snapshot => snapshot.val());
}
const RoomView = () => {
const [homeId, setHomeId] = useState(0);
const [status, setStatus] = useState("loading")
const [roomList, setRoomList] = useState([]);
const [user, userLoading] = useUser();
useEffect(() => {
if (userLoading) // loading user state, do nothing
return;
if (!user) { // user is signed out, reset to empty state
setHomeId(-1);
setRoomList([]);
setStatus("signed-out");
return;
}
let disposed = false;
setStatus("loading");
const doAsyncWork = async () => {
const newHomeId = await getUserHomeId(user.uid);
const roomsData = await getHomeRoomData(newHomeId);
const newRoomList = [];
snapshot.forEach(roomSnapshot => {
const title = roomSnapshot.key;
const apps = [];
roomSnapshot.forEach(appSnapshot => {
apps.push({
key: appSnapshot.key,
...appSnapshot.val()
});
});
newRoomList.push({
key: title,
title,
apps
});
});
if (disposed) // component unmounted? don't update state
return;
setHomeId(newHomeId);
setRoomList(newRoomList);
setStatus("ready");
}
doAsyncWork()
.catch(err => {
if (disposed) // component unmounted? silently ignore
return;
// TODO: Handle error better than this
console.error("Failed!", err);
setStatus("error");
});
return () => disposed = true;
}, [user, userLoading]); // rerun only if user state changes
switch (status) {
case "loading":
return <Spin tip="Loading rooms..." />
case "error":
return <Alert
message="Error"
description="An unknown error has occurred"
type="error"
/>
case "signed-out":
return <Alert
message="Error"
description="User is signed out"
type="error"
/>
}
if (roomList.length === 0) {
return <Alert
message="No rooms found"
description="You haven't created any rooms yet"
type="info"
/>
}
return (
<Tabs defaultActiveKey={roomList[0].key}>
{
roomList.map(room => {
let tabContent;
if (room.apps.length == 0) {
tabContent = "No apps found in this room";
} else {
tabContent = room.apps.map(app => {
<Card style={{ width: 300, marginTop: 16 }} key={app.key}>
<Meta
avatar={
<Avatar src="https://via.placeholder.com/300x300?text=Icon" />
}
title={app.name}
description={app.description}
/>
</Card>
});
}
return <TabPane tab={room.title} key={room.key}>
{tabContent}
</TabPane>
})
}
</Tabs>
);
};
I have a main screen where you can see multiple items displayed in card form you can access an item by pressing it to see its details, on the details screen I added a bookmark option that save the item using async storage, you can also check the saved items on a different screen called savedItems screen.
The problem is :
when i bookmark an item it get saved properly and i can go to the savedItems screen and find it there, but some times i have to reload the app for the item to appear on the savedItems screen why is that ?
if i book multiple items they all get saved ( on console.log ) but only the last one appears for some reason i never get more then one item displayed on the SavedItems screen
Bellow is snippets of the code used to book mark ( saved an item on its details screen )
Details.js
const DetailScreen = (props) => {
const [saved, setSaved] = useState([]);
const [items, setItems] = useState(props.route.params);
const onSave = (item) => {
const newItems = [...saved, item];
setSaved(newItems);
const items = JSON.stringify(newItems);
SaveItem("saved", items).then((res) => {
console.log("saved", res);
});
};
const goToDetails = () => {
setSaved([]);
props.navigation.navigate("SaveScreen");
};
const { width, height } = Dimensions.get("window");
const { data } = props.route.params; // this returns the data from each article
//ReadItem("saved").then((res) => console.log(res));
return (
<TouchableOpacity
...
onPress={() => {
onSave(data);
}}
>
<MaterialCommunityIcons
name="bookmark"
size={35}
color={colors.shade2}
/>
</TouchableOpacity>
)
SaveScreen.js
export default class Details extends Component {
state = {
saved: [],
};
removeItem = () => {
DeleteItem("saved")
.then((res) => {
this.setState({
saved: [],
});
console.log(res);
})
.catch((e) => console.log(e));
};
componentDidMount = () => {
ReadItem("saved")
.then((res) => {
if (res) {
const saved = JSON.parse(res);
this.setState({
saved: saved,
});
}
})
.catch((e) => console.warn(e));
};
render() {
return (
<View style={styles.container}>
<FlatList
keyExtractor={(item, index) => index.toString()}
data={this.state.saved}
renderItem={({ item }) => {
return (
<TouchableScale
activeScale={0.9}
tension={50}
friction={7}
useNativeDriver
onPress={() =>
this.props.navigation.navigate("DetailScreen", { data: item })
}
>
<Card item={item} />
</TouchableScale>
);
}}
/>
{this.state.saved.length > 0 && (
<TouchableOpacity onPress={this.removeItem} style={styles.button}>
<Text style={styles.save}>Remove Key</Text>
</TouchableOpacity>
)}
</View>
);
}
}
the code used to save data using async storage
Dbhelper.js
import { AsyncStorage } from "react-native";
export const SaveItem = async (key, value) => {
try {
await AsyncStorage.setItem(key, value);
console.log("saved");
} catch (e) {
console.log(e);
}
};
export const ReadItem = async (key) => {
try {
var result = await AsyncStorage.getItem(key);
return result;
} catch (e) {
return e;
}
};
export function MultiRead(key, onResponse, onFailure) {
try {
AsyncStorage.multiGet(key).then((values) => {
let responseMap = new Map();
values.map((result, i, data) => {
let key = data[i][0];
let value = data[i][1];
responseMap.set(key, value);
});
onResponse(responseMap);
});
} catch (error) {
onFailure(error);
}
}
export async function DeleteItem(key) {
try {
await AsyncStorage.removeItem(key);
return true;
} catch (exception) {
return false;
}
}
if i book multiple items they all get saved ( on console.log ) but only the last one appears for some reason i never get more then one item displayed on the SavedItems screen
I think the problem is when you save new item you remove old items. You have to read old items when you mount DetailScreen. Or you can read items inside onSave method. It's more safety. It depends your application architecture
const DetailScreen = (props) => {
const [items, setItems] = useState(props.route.params);
const onSave = (item) => {
let saved = [];
ReadItem("saved")
.then((res) => {
if (res) {
saved = JSON.parse(res);
}
const newItems = [...saved, item];
const items = JSON.stringify(newItems);
SaveItem("saved", items).then((res) => {
console.log("saved", res);
});
})
.catch((e) => console.warn(e));
};
const goToDetails = () => {
setSaved([]);
props.navigation.navigate("SaveScreen");
};
const { width, height } = Dimensions.get("window");
const { data } = props.route.params; // this returns the data from each article
//ReadItem("saved").then((res) => console.log(res));
return (
<TouchableOpacity
...
onPress={() => {
onSave(data);
}}
>
<MaterialCommunityIcons
name="bookmark"
size={35}
color={colors.shade2}
/>
</TouchableOpacity>
)
when i bookmark an item it get saved properly and i can go to the savedItems screen and find it there, but some times i have to reload the app for the item to appear on the savedItems screen why is that ?
I have only one idea why it may happens. In some cases, you don't unmount SaveScreen. When you return to the SaveScreen componentDidMount is not called. Just try to add console.log to componentDidMount and when you reproduce this behavior if console.log is missed it means you SaveScreen wasn't unmounted
To handle all cases you can try to do that:
export default class Details extends Component {
state = {
saved: [],
};
removeItem = () => {
DeleteItem("saved")
.then((res) => {
this.setState({
saved: [],
});
console.log(res);
})
.catch((e) => console.log(e));
};
componentDidMount = () => {
this.readItems();
this.unsubscribe = navigation.addListener('focus', this.readItems);
};
componentWillUnmount() {
this.unsubscribe();
}
readItems = () => {
ReadItem("saved")
.then((res) => {
if (res) {
const saved = JSON.parse(res);
this.setState({
saved: saved,
});
}
})
.catch((e) => console.warn(e));
}
render() { ... }
}
I'm trying to build a custom hook to fetch data.
This is my App.js:
const [loadData, mainCategories] = useCustomHook();
useEffect(() => {
loadData();
}, []);
return (
<FlatList
data={mainCategories}
renderItem={({ item }) => {
return (
<View>
<Text>{item.name}</Text>
</View>
);
}}
/>
);
};
This is my useCustomHook.js:
const useCats = () => {
const [mainCategories, setMainCategories] = useState();
const loadData = async () => {
let response = await fetch(
`URL/read.php`
);
let results = await response.json();
setMainCategories(results.data);
};
return [loadData, mainCategories];
};
export default useCustomHook;
It''s working fine. I'm returning and exporting loadData and mainCategories from my useCustomHook.js and I'm getting both in App.js to use it there.
Now comes the part I'm struggling with:
Let's say I need a constant from App.js in the useCustomHook.js. In my case I'm getting an ID in App.js (forwarded by react-navigation from the previous screen) which I need in the URL like so:
URL/read.php?id=${id}
Is there any way to get that ID in useCustomHook.js?
You can pass it as a parameter. Try below
const useCats = () => {
const [mainCategories, setMainCategories] = useState();
const loadData = async (id) => {
let response = await fetch(
`URL/read.php?id=${id} `
);
let results = await response.json();
setMainCategories(results.data);
};
return [loadData, mainCategories];
};