React-Native — Building a dynamic URL for API requests using Promise - javascript

I am breaking down a larger post into smaller questions. Please understand I never used Promise before and that I am new to React-Native too. It would be great to get feedback and recommendations on how to setup API calls and handle the data. Thank you in advance.
How can I dynamically create URLs for API requests? Here's what I am trying to achieve:
Pseudocode
Child
Retrieve two variables
Use these two variables to build an URL
Trigger the first Promise and resolve
Retrieve another two variables
Use these two variables to build a new an URL
Trigger the second Promise and resolve
Gather the data from both promises and pass to parent
Parent
Retrieve data from Child
Get data from the first Promise and set to a state
Get data from the second Promise and set to another state
APIservice.js
Child
class APIservice {
_getStopPoint = (endpoint) => {
return new Promise(function(resolve, reject) {
fetch(endpoint)
.then((response) => response.json())
.then((data) => {
console.log("APIservice StopPoint", data)
resolve(data);
});
});
};
};
module.exports = new APIservice
List.js
Parent
As you can see, the way I setup the endpoint is lame. It's not ideal as the URL is the same. I want to structure something that can receive two variables and build the URL on the go. Something like https://api.tfl.gov.uk/Line/${routeid}/Arrivals/${stationid}.
If I manage that, how can I pass the API call to the APIservice having only one endpoint that dynamically will change based on the two variables it receives? I am not sure how to differentiate the call in the Promise.all having only "one" URL.
let APIservice = require('./APIservice')
let endpoint = 'https://api.tfl.gov.uk/Line/55/Arrivals/490004936E'
let endpoint1 = 'https://api.tfl.gov.uk/Line/Northern/Arrivals/940GZZLUODS'
export class List extends Component {
constructor(props) {
super(props);
this.state = {
bus: null,
tube: null,
}
};
componentWillMount() {
let loadData = (endPoint) => {
Promise.all([
APIservice._getStopPoint(endpoint),
APIservice._getStopPoint(endpoint1),
])
.then((data) => {
// Name for better identification
const listBus = data[0]
const listTube = data[1]
this.setState({
bus: listBus,
tube: listTube
}, () => {
console.log("bus", this.state.bus, "tube", this.state.tube)
});
})
.catch((error) => {
console.log(error)
})
}
loadData(endpoint);
loadData(endpoint1);
}
render() {
return(
<View>
<FlatList
data={this.state.bus}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
<FlatList
data={this.state.tube}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
</ View>
);
}
};

It is pretty easy to implement what you are saying once you understand how this works.
You are using fetch for your API calls which returns a Promise upon use. The pseudo-code for your use case would be something like this:
class APIService {
static fetchFirst(cb) {
fetch('FIRST_URL')
.then(resp => {
try {
resp = JSON.parse(resp._bodyText);
cb(resp);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
static fetchSecond(routeid, stationid, cb) {
fetch(`https://api.tfl.gov.uk/Line/${routeid}/Arrivals/${stationid}`)
.then(resp => {
try {
resp = JSON.parse(resp._bodyText);
cb(resp);
} catch(e) {
cb(e);
}
})
.catch(e => cb(e));
}
}
module.exports = APIService;
Include this in your parent component and use it as follows:
let APIService = require('./APIService')
export class List extends Component {
constructor(props) {
super(props);
this.state = {
bus: null,
tube: null,
}
};
componentWillMount() {
APIService.fetchFirst((resp1) => {
APIService.fetchSecond(resp1.routeid, resp1.stationid, (resp2) => {
this.setState({
tube: resp2
});
});
});
}
render() {
return(
<View>
<FlatList
data={this.state.bus}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
<FlatList
data={this.state.tube}
renderItem={({item}) => (
<Text>{item.timeToStation}</ Text>
)}
keyExtractor={item => item.id}
/>
</ View>
);
}
};
I haven't checked the errors on the callback function, please see that the errors are handled when you use this.

Related

React Native Flatlist not refreshing data on pull up

Here is my posts page code, it fetches post titles from my API on load and this works perfect. The problem is that once it's loaded if a new post is added to API and I pull up to refresh it doesn't get new posts even though my onRefresh function works because I can trigger an alert in it.
The only way I can get new posts in API to show after they were loaded is by reloading the application itself.
componentDidMount() {
this.fetchData()
}
constructor(props) {
super(props);
this.state = {
refreshing: true,
data: []
};
}
fetchData = () => {
const url = 'myAPIurl';
fetch(url)
.then(res => {
return res.json()
})
.then(res => {
const arrayData = [...this.state.data, ...res]
this.setState({
data: arrayData,
refreshing: false
});
})
.catch(error => {
console.log(error);
this.setState({ refreshing: false });
});
};
handleRefresh = () => {
this.setState(
{
refreshing: true
},
() => {
this.fetchData();
alert('Pulled Up to Refresh');
}
);
};
render() {
return (
<View>
<FlatList
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
horizontal={false}
data={this.state.data}
keyExtractor={item => item.id}
renderItem={({ item }) =>
<View>
<Text>{item.title.rendered}</Text>
</View>
}
/>
</View>
);
}
}
When I pull up to refresh I get this warning: Two children with same key. Keys should be unique. This is weird because each post ID is unique. And even with this warning, the new posts that are in API don't show unless I re-load the application.
Change your handleRefresh function like below:
handleRefresh = () => {
this.setState(
{
refreshing: true,
data:[]
},
() => {
this.fetchData();
alert('Pulled Up to Refresh');
}
);
};

How to display multiple sets of data with reduce and promise.all in React

I'm making two calls from an API. I want to display the top results for airing shows and top tv shows. I have all of the data being returned from both API calls, but my code isn't efficient. I'd like to somehow take my returned data and display it in a single component (TopAnime) that will then map and return the information provided.
I figured reduce would be the best route, but I'm fumbling at this point. My thought process was to reduce the returned data from the API into an array. Take that reduced array and pass it as my new state and then have my component display it without having to write duplicate code. Both topTv and topAIring are showing because I've written the component twice, but it's clearly not best practice to repeat code.
class HomePage extends Component {
state = {
topTv: [],
topAiring: []
}
async getData() {
const api = "https://api.jikan.moe/v3"
const urls = [
`${api}/top/anime/1/tv`,
`${api}/top/anime/1/airing`
];
return Promise.all(
urls.map(async url => {
return await fetch(url) // fetch data from urls
})
)
.then(responses => // convert response to json and setState to retrieved data
Promise.all(responses.map(resp => resp.json())).then(data => {
console.log("data", data)
// const results = [...data[0].top, ...data[1].top]; // data from TV & data from airing
const reduceResults = data.reduce((acc, anime) => {
return acc + anime
}, [])
console.log('reduce', reduceResults);
const tvResults = data[0].top // data from TV
const airingResults = data[1].top // data from airing
this.setState({
topTv: tvResults,
topAiring: airingResults
});
})
)
.catch(err => console.log("There was an error:" + err))
}
componentDidMount() {
this.getData();
}
render() {
return (
<HomeWrapper>
<h2>Top anime</h2>
<TopAnime>
{this.state.topTv.map((ani) => {
return (
<div key={ani.mal_id}>
<img src={ani.image_url} alt='anime poster' />
<h3>{ani.title}</h3>
</div>
)
}).splice(0, 6)}
</TopAnime>
<h2>Top Airing</h2>
<TopAnime>
{this.state.topAiring.map((ani) => {
return (
<div key={ani.mal_id}>
<img src={ani.image_url} alt='anime poster' />
<h3>{ani.title}</h3>
</div>
)
}).splice(0, 6)}
</TopAnime>
</HomeWrapper>
)
}
}
Since the response from API contains a flag called rank you can use the Array.prototype.filter to only show shows ranked 1-6.
Working demo here
import React, { Component } from "react";
import { TopAnime } from "./TopAnime";
export class HomePage extends Component {
state = {
topTv: [],
topAiring: []
};
async getData() {
const api = "https://api.jikan.moe/v3";
const urls = [`${api}/top/anime/1/tv`, `${api}/top/anime/1/airing`];
return Promise.all(
urls.map(async url => {
return await fetch(url); // fetch data from urls
})
)
.then((
responses // convert response to json and setState to retrieved data
) =>
Promise.all(responses.map(resp => resp.json())).then(data => {
// if you care about mutation use this
const topTvFiltered = data[0].top.filter( (item) => item.rank <= 6 );
const topAiringFiltered = data[1].top.filter( (item) => item.rank <= 6 );
this.setState({
topTv: topTvFiltered,
topAiring: topAiringFiltered
});
})
)
.catch(err => console.log("There was an error:" + err));
}
componentDidMount() {
this.getData();
}
render() {
const { topTv, topAiring } = this.state;
return (
<React.Fragment>
{ topTv.length > 0 ? <h2>Top TV</h2> : null }
{this.state.topTv.map((item, index) => (
<TopAnime key={index} title={item.title} image={item.image_url} />
))}
{ topAiring.length > 0 ? <h2>Top airing</h2> : null }
{this.state.topAiring.map((item, index) => (
<TopAnime key={index} title={item.title} image={item.image_url} />
))}
</React.Fragment>
);
}
}

React render component asynchronously, after data is fetched

I need to render a component after data is fetched. If try to load data instantly, component gets rendered but no data is show.
class App extends React.Component {
//typical construct
getGames = () => {
fetch(Url, {})
.then(data => data.json())
.then(data => {
this.setState({ links: data });
})
.catch(e => console.log(e));
};
componentDidMount() {
this.getGames();
}
render() {
return (
<div className="App">
<Game gameId={this.state.links[0].id} /> //need to render this part
after data is received.
</div>
);
}
}
You could keep an additional piece of state called e.g. isLoading, and render null until your network request has finished.
Example
class App extends React.Component {
state = { links: [], isLoading: true };
getGames = () => {
fetch(Url, {})
.then(data => data.json())
.then(data => {
this.setState({ links: data, isLoading: false });
})
.catch(e => console.log(e));
};
componentDidMount() {
this.getGames();
}
render() {
const { links, isLoading } = this.state;
if (isLoading) {
return null;
}
return (
<div className="App">
<Game gameId={links[0].id} />
</div>
);
}
}
You can do like this using short circuit.
{
this.state.links && <Game gameId={this.state.links[0].id} />
}
Can we use the pattern of "Render-as-you-fetch" to solve the problem.
Using a flag to check whether loading is complete doesn't look like a clean solution..

Invariant violation : App(...) Nothing was returned from render

I am trying to render component based on the promise from AsyncStorage and I am getting this error:
Exception: Invariant Violation: App(...): Nothing was returned from
render. This usually means a return statement is missing. Or, to
render nothing, return null.
I know for sure the components are good because I tried to render each of them individually and they're both worked just fine, it's when Im trying to render it through AsyncStorage.getItem I cant make it done.
CODE:
checkToken = () => {
AsyncStorage.getItem('user')
.then((res) => {
res = JSON.parse(res)
console.log('this is checkToken()',res.token)
// this.renderUserSection(res.token);
return res.token;
})
.then((token) => {
return (
<View style={styles.container}>
{!token ? this.renderUser() : this.renderApp()}
</View>
)
})
.catch((err) => {
console.log(err)
})
}
render() {
return (
this.checkToken()
)
}
When I did :
render() {
return (
this.renderUser()
)
}
It worked great!
Also when I did:
render() {
return (
this.renderApp()
)
}
It worked as well, so the functions are good, is the logic or something at the top one that I can't make work.
AsyncStorage is asynchronous, so it doesn't immediately return the view you have written. You need to display a spinner or a custom view until AsyncStorage has retrieved your data.
Here's a solution:
constructor() {
this.state = {
token: null
}
this.checkToken();
}
checkToken = () => {
AsyncStorage.getItem('user')
.then((res) => {
res = JSON.parse(res)
console.log('this is checkToken()',res.token)
// this.renderUserSection(res.token);
})
.then((token) => {
this.setState({token: token})
})
.catch((err) => {
console.log(err)
})
}
render() {
if (this.state.token == null) {
return (<View> {this.renderUser()} </View>)
}
else {
return (
<View style={styles.container}>
{this.renderApp()}
</View>
)
}
}
Please use state to store the token first. (The method you have 'checkToken' is a void method)
checkToken = () => {
AsyncStorage.getItem('user')
.then((res) => {
res = JSON.parse(res)
console.log('this is checkToken()',res.token)
// this.renderUserSection(res.token);
return res.token;
this.setState(state => ({
...state,
token : res.token
}))
})
.catch((err) => {
console.log(err)
})
}
render () {
const { token } = this.state;
return (
<View style={styles.container}>
{
!token
? this.renderUser()
: this.renderApp()
}
</View>
)
}
Well, checkToken is a function that:
calls AsyncStorage
returns undefined
So if the logs tell you that render returned nothing, the logs are right!
You should add a return statement to your checkToken.
Then I suppose we should get to part 2, now check token is a function that:
calls AsyncStorage
returns a Promise that the executed function will be returned
It does not really work... React renders components, not promises!
My take is that you need to refactor how this works. My suggestion:
refactor checkToken into a function that, if getItem succeeds, calls setState({ content: })
if it fails, setState({ content: null })
in your render function, do not return the function call. Instead, return state.content
IF you want to check the token AT EVERY RENDER, call checkToken just before returning state.content from render.
IF you want to check the token just once, call checkToken in componentDidMount instead - or wherever you need.
This is a sample of what I think the code should look like. You should think about what's the best place to call checkToken.
checkToken = () => {
let self = this;
AsyncStorage.getItem('user')
.then((res) => {
res = JSON.parse(res)
console.log('this is checkToken()',res.token)
// this.renderUserSection(res.token);
return res.token;
})
.then((token) => {
self.setState({
content: (<View style={styles.container}>
{!token ? self.renderUser() : self.renderApp()}
</View>)
});
})
.catch((err) => {
self.setState({ content: null })
console.log(err)
})
}
render() {
//are you sure you need to call checkToken here? Maybe there's a better place? if there is no shouldComponentUpdate this could cause an infinite re-render loop
//you could maybe call it for componentDidMount, or some other lifecycle method?
//otherwise I think you'll need that shouldComponentUpdate implementation.
this.checkToken();
return this.state.content;
}

React .map is not a function

I'm trying to learn React and I'm a beginner when it comes to Javascript. Right now I'm working on an app that is fetching data from Flickr's API. The problem is that when I try to use the map method on the props in the Main.js component I get an error saying "Uncaught TypeError: this.props.photos.map is not a function". After searching here on Stackoverflow I think the problem is that this.props are javascript objects and not an array. The problem is that I can't figure out how to make it an array. Can anyone explain what I'm doing wrong?
My code:
class App extends Component {
constructor() {
super();
this.state = {
}
}
componentDidMount() {
let apiKey = 'xxxxxxxxxxxxxxxxxx';
let searchKeyword = 'nature';
let url = `https://api.flickr.com/services/
rest/?api_key=${apiKey}&method=flickr.photos.
search&format=json&nojsoncallback=1&&per_page=50
&page=1&text=${searchKeyword}`;
fetch(url)
.then(response => response.json())
.then(data => data.photos.photo.map((x) => {
this.setState({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server})
// console.log(this.state)
}))
}
render() {
return (
<div className="App">
<Header />
<Main img={this.state.photos} />
<Navigation />
</div>
);
}
}
export default class Main extends Component {
render() {
return(
<main className="main">
{console.log(this.props.photos)}
</main>
)
}
}
Edit:
Why is this.props.img undefined first?
Screen shot from console.log(this.props.img)
fetch(url)
.then(response => response.json())
.then(data => data.photos.photo.map((x) => {
this.setState({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server})
}))
What is happening is that your map function in your promise is resetting the component's state for every photo that is returned. So your state will always be the last object in your list of returned photos.
Here is a more simplified example of what I am referring to
const testArray = [1,2,3,4];
let currentState;
testArray.map((value) => currentState = value)
console.log(currentState);
What you want to do is this
const testArray = [1,2,3,4];
let currentState;
//Notice we are using the return value of the map function itself.
currentState = testArray.map((value) => value)
console.log(currentState);
For what you are trying to accomplish, you want your state to be the result of the map function (since that returns an array of your results from the map). Something like this:
fetch(url)
.then(response => response.json())
.then(data =>
this.setState({
photos:
data.photos.photo.map((x) => ({
farm: x.farm,
id: x.id,
secret: x.secret,
server: x.server
}))
})
)
This error might also happen if you try to provide something else other than the array that .map() is expecting, even if you declare the variable type properly. A hook-based example:
const [myTwinkies, setMyTwinkies] = useState<Twinkies[]>([]);
useEffect(() => {
// add a twinky if none are left in 7eleven
// setMyTwinkies(twinkiesAt711 ?? {}); // ---> CAUSES ".map is not a function"
setMyTwinkies(twinkiesAt711 ?? [{}]);
}, [twinkiesAt711, setMyTwinkies]);
return (<ul>
{myTwinkies.map((twinky, i)=> (
<li key={i}>Twinky {i}: {twinky?.expiryDate}</li>
))}
</ul>)
Just check the length of the array before going for the map. If the len is more than 0 then go for it, otherwise, ignore it.
data.photos.photo.map.length>0 && data.photos.photo.map(........)

Categories