I'm trying to call a weather API with coordinates recieved from the user's computer using navigator.geolocation.getCurrentPosition()
My problem is I'm having trouble figuring out the right way to do this given everything is running asyncronously. Then, I thought I came up with a decent work-around using componentDidMount(), but unfortunately it didn't work.
Here's my codepen (sans API key)
And here's the code:
state = {
data: "Please wait while you're weather is loading...",
}
componentDidMount() {
this.state.hasOwnProperty('uri') ?
fetch(this.state.uri).then((res) => res.json())
.then((data) => this.setState({
data: {
tempString: data.current_observation.temperature_string,
realTemp: data.current_observation.feelslike_string,
skyImg: data.current_observation.icon_url.substring(0, 4) + 's' + data.current_observation.icon_url.substring(4),
location: data.current_observation.display_location.full,
temp_f: data.current_observation.temp_f,
}
}))
.catch((err) => console.log(err.message))
: navigator.geolocation.getCurrentPosition((pos) => {
this.setState({
uri: "https://api.wunderground.com/api/{API GOES HERE}/conditions/q/" + pos.coords.latitude.toString() + "," + pos.coords.longitude.toString() + ".json"
})
console.log(this.state.uri)
})
}
My understanding of how everything is running is as follows:
Initial component renders
componentDidMount() is called and takes a look at the if statement
can't find the URI property, so starts the getCurrentPosition() call, setting state with the new URI property (to my knowledge, this.setState should trigger a re-render, and then...)
componentDidMount() runs again, but this time finds the URI property
for some unknown reason, fetch() isn't running
Though I'm not sure, my best guess is although there is now URI property, by time the new componentDidMount() runs, the program is still in the process of figuring out what to set it as. But I might be completely wrong. I could've also created an infinite loop where componentDidMount() never sees a URI property and continuously re-renders.
As #brub said: componentDidMount doesn't run more than once, no matter how much the ui is updated. I ended up using componentDidUpdate as a solution. Here's the code now:
state = {
data: "Please wait while you're weather is loading...",
}
componentDidMount() {
navigator.geolocation.getCurrentPosition((pos) => {
this.setState({
uri: "https://api.wunderground.com/api/{API GOES HERE}/conditions/q/" + pos.coords.latitude.toString() + "," + pos.coords.longitude.toString() + ".json"
})
})
}
componentDidUpdate() {
fetch(this.state.uri).then((res) => res.json())
.then((data) => this.setState({
data: {
tempString: data.current_observation.temperature_string,
realTemp: data.current_observation.feelslike_string,
skyImg: data.current_observation.icon_url.substring(0, 4) + 's' + data.current_observation.icon_url.substring(4),
location: data.current_observation.display_location.full,
temp_f: data.current_observation.temp_f,
}
}))
.catch((err) => console.log(err.message))
}
Related
Edit: Moved the getMetadata() call out of render, as suggested by #Robin Zigmond. Still having trouble getting the proper value from getMetadata() returned, but that's different from my original question. Updating code just for reference.
Quick background: I have a fair amount of experience with shell scripting and stuff like Perl and PHP, but javascript and especially the libraries on top of it like React feel very foreign to me. Forcing myself to develop this with React to teach myself something new, so if you see any bad practices, feel free to suggest improvements!
That said, I'm trying to write a simple app that:
Prompts for search parameter
Runs query against 3rd party service that returns json results
For each returned element, run additional query to get more details
Display tabular results
I have 1, 2, and 4 generally worked out, but struggling with 3. Here's what I have so far, with less important code snipped out:
class VGSC_Search extends React.Component {
constructor() {
super();
this.state = {
submitted: false,
platform: '',
games: [],
metadata: [],
files: [],
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({platform: event.target.value});
}
handleSubmit(event) {
this.setState({submitted: true});
<SNIP - vars>
fetch(encodeURI(searchurl + query + fields + sort + rows))
.then(result => result.json())
.then(data => this.setState({
games: data.response.docs.map(game => ({
identifier: game.identifier,
title: game.title,
creator: game.creator,
year: game.year,
uploader: this.getMetadata(game.identifier),
}))
}));
event.preventDefault();
}
getMetadata(id) {
<SNIP - vars>
fetch(encodeURI(metadataurl + id + metadatainfo))
.then(result => result.json())
.then(data => this.setState({metadata: data.response}));
}
renderResults() {
const {games} = this.state;
const {metadata} = this.state;
return (
<SNIP - table header>
<tbody>{games.map(game =>
<tr key={game.identifier}>
<td><a href={'https://archive.org/details/' + game.identifier}>{game.title}</a></td>
<td>{game.creator}</td>
<td>{game.year}</td>
<td>{game.uploader}</td>
</tr>
)}</tbody>
</table>
);
}
render(){
return (
<div>
<SNIP - form>
<br/>
{this.state.submitted && this.renderResults()}
</div>
);
}
}
My problem is with that Uploader field, which should run getMetadata() on the given identifier and return the name of the uploader. I'm having trouble figuring out how to reference the result, but my biggest problem is actually that my browser keeps running getMetadata() on all displayed items in an endless loop. Eg, from the Developer Tools log:
XHR GET https://archive.org/metadata/4WheelThunderEuropePromoDiscLabel/metadata/uploader [HTTP/1.1 200 OK 105ms]
XHR GET https://archive.org/metadata/AirJapanCompleteArtScans/metadata/uploader [HTTP/1.1 200 OK 279ms]
XHR GET https://archive.org/metadata/AeroWings-CompleteScans/metadata/uploader [HTTP/1.1 200 OK 287ms]
XHR GET https://archive.org/metadata/BioCodeVeronicaLEDCT1210MNTSCJ/metadata/uploader [HTTP/1.1 200 OK 279ms]
XHR GET https://archive.org/metadata/Biohazard2ValuePlusDreamcastT1214MNTSCJ/metadata/uploader [HTTP/1.1 200 OK 282ms]
XHR GET https://archive.org/metadata/4WheelThunderEuropePromoDiscLabel/metadata/uploader [HTTP/1.1 200 OK 120ms]
XHR GET https://archive.org/metadata/AirJapanCompleteArtScans/metadata/uploader [HTTP/1.1 200 OK 120ms]
<SNIP>
The first search returns 5 results, and getMetadata() is run correctly on those five results, but note that it starts repeating. It'll do that endlessly until I reload the page.
I'm guessing that has something to do with running getMetadata() inside a function that's being rendered, but I'm not sure why, and having trouble thinking of a good alternate way to do that.
Can anyone explain why I'm seeing this behavior, and (hopefully) offer a suggestion on how to properly implement this?
Thanks!
The infinite loop happens because you're running getMetadata inside render (or rather inside a function that's called from render) - and that results in changing state, which causes a rerender, and so on in an endless loop.
It's bad practice in any situation to call any function which cause any "side effects" from within render - render should simply determine the output given the component's props and state. Typically data-fetching like you're doing is done inside componentDidMount and/or componentDidUpdate. However in this case, where you appear to need to fetch additional data based on the first response in handleSubmit, it seems that you need to call getMetadata from within the final .then callback of that function.
After your latest edit, I can see the problem with the approach you tried here. this.getMetadata doesn't actually return anything. To fix it, you can return the Promise returned by fetch:
getMetadata(id) {
<SNIP - vars>
return fetch(encodeURI(metadataurl + id + metadatainfo))
.then(result => result.json())
.then(data => this.setState({metadata: data.response}));
}
and then use asyc/await inside handleSubmit, with Promise.all to manage the array:
fetch(encodeURI(searchurl + query + fields + sort + rows))
.then(result => result.json())
.then(async data => {
const games = await Promise.all(data.response.docs.map(async game => ({
identifier: game.identifier,
title: game.title,
creator: game.creator,
year: game.year,
uploader: await this.getMetadata(game.identifier),
})));
this.setState({ games });
});
I'm trying to run a function that needs some data that I get back from the mounted method. Right now I try to use computed to create the function but unfortunately for this situation computed runs before mounted so I don't have the data I need for the function. Here is what I'm working with:
computed: {
league_id () {
return parseInt(this.$route.params.id)
},
current_user_has_team: function() {
debugger;
}
},
mounted () {
const params = {};
axios.get('/api/v1/leagues/' +this.$route.params.id, {
params,
headers: {
Authorization: "Bearer "+localStorage.getItem('token')
}
}).then(response => {
debugger;
this.league = response.data.league
this.current_user_teams = response.data.league
}).catch(error => {
this.$router.push('/not_found')
this.$store.commit("FLASH_MESSAGE", {
message: "League not found",
show: true,
styleClass: "error",
timeOut: 4000
})
})
}
As you can see I have the debugger in the computed function called current_user_has_team function. But I need the data I get back from the axios call. Right now I don't have the data in the debugger. What call back should I use so that I can leverage the data that comes back from the network request? Thank You!
If your computed property current_user_has_team depends on data which is not available until after the axios call, then you need to either:
In the current_user_has_team property, if the data is not available then return a sensible default value.
Do not access current_user_has_team from your template (restrict with v-if) or anywhere else until after the axios call has completed and the data is available.
It's up to you how you want the component to behave in "loading" situations.
If your behavior is synchronous, you can use beforeMount instead of mounted to have the code run before computed properties are calculated.
I have a component that must make an HTTP request based off new props. Currently it's taking a while to actually update, so we've implemented a local store that we'd like to use to show data from past requests and then show the HTTP results once they actually arrive.
I'm running into issues with this strategy:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.setState({data:this.makeHttpRequest(nextProps.dataToGet)});
//triggers single render, only after request gets back
}
What I think is happening is that react bundles all the setstates for each lifecycle method, so it's not triggering render until the request actually comes back.
My next strategy was this:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.go=true;
}
componentDidUpdate(){
if(this.go){
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
}
this.go=false;
}
//triggers two renders, but only draws 2nd, after request gets back
This one SHOULD work, it's actually calling render with the localstore data immediately, and then calling it again when the request gets back with the request data, but the first render isnt actually drawing anything to the screen!
It looks like react waits to draw the real dom until after componentDidUpdate completes, which tbh, seems completely against the point to me.
Is there a much better strategy that I could be using to achieve this?
Thanks!
One strategy could be to load the data using fetch, and calling setState when the data has been loaded with the use of promises.
componentWillRecieveProps(nextProps){
this.loadData(nextProps)
}
loadData(nextProps){
// Create a request based on nextProps
fetch(request)
.then(response => response.json())
.then(json => this.setState({updatedValue: json.value})
}
I use the pattern bellow all the time (assuming your request function supports promises)
const defaultData = { /* whatever */ }
let YourComponent = React.createClass({
componentWillRecieveProps: function(nextProps) {
const that = this
const cachedData = this.getDataFromLocalStore(nextProps)
that.setState({
theData: { loading: true, data: cachedData }
})
request(nextProps)
.then(function(res) {
that.setState({
theData: { loaded: true, data: res }
})
})
.catch(function() {
that.setState({
theData: { laodingFailed: true }
})
})
},
getInitialState: function() {
return {
theData: { loading: true, data: defaultData }
};
},
render: function() {
const theData = this.state.theData
if(theData.loading) { return (<div>loading</div>) } // you can display the cached data here
if(theData.loadingFailed) { return (<div>error</div>) }
if(!theData.loaded) { throw new Error("Oups") }
return <div>{ theData.data }</div>
}
)}
More information about the lifecycle of components here
By the way, you may think of using a centralized redux state instead of the component state.
Also my guess is that your example is not working because of this line:
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
It is very likely that makeHttpRequest is asynchronous and returns undefined. In other words you are setting your data to undefined and never get the result of the request...
Edit: about firebase
It looks like you are using firebase. If you use it using the on functions, your makeHttpRequest must look like:
function(makeHttpRequest) {
return new Promise(function(resolve, reject) {
firebaseRef.on('value', function(data) {
resolve(data)
})
})
}
This other question might also help
I am now using react-http-request in my React.js component to send request and process the response. The URL parameter passed is relevant to the component state such that when the state changes, the component will be re-rendered and change the component display.
This works on the first request. However, I found that the component does not return a {load: true} after the second request, and I wonder how to solve this.
I tried to call the onRequest method and set the loading state for the component, but I cannot change the loading state after the request is finished (as render function cannot change the state).
react-http-request: https://github.com/mbasso/react-http-request
My Code is like below:
var FilmList = React.createClass({
getInitialState: function(){
return {
queryType: this.props.queryType
}
},
// ... details emitted.
render: function(){
return (<Request
url={config.url.api + "/" + this.state.queryType}
method="get"
accept="application/json"
query={{ several parameter }}
>
{
({error, result, loading}) => {
if (loading || error) {
return <Loading />
}
else {
// process the result here.
}
}
}
</Request>)
}
So, my initial recommendation would be that you use some state management library (redux, mobx, etc) but it is not necessary to get a working example of your code, so:
import fetch from 'whatwg-fetch'; // gives compatibility with older browsers
var FilmList = React.createClass({
getInitialState: function(){
return {
queryType: this.props.queryType,
content: null
}
},
componentWillMount: function() {
this.fetchContent();
},
fetchContent: function() {
const uri = config.url.api + "/" + this.state.queryType;
// You can use w/e you want here (request.js, etc), but fetch is the latest standard in the js world
fetch(uri, {
method: 'GET',
// More properties as you see fit
})
.then(response => response.json()) // might need to do this ;)
.then(response => {
this.setState({
content: response
})
})
},
// ...
render: function(){
const content = this.state.content? (
// render your content based on this.state.content
): (
<Loading />
)
return content;
}
});
Haven't tested this code, but there are some nice benefits to it:
The http request is not dependant on React, which should (in theory) be for UI components.
The fetching mechanism is decoupled, and can be re-used at any point in the component lifecycle
In my opinion easier to read, divided into smaller logical chunks
I would recommend reading the React Component Lifecycle.
In this case, I read the source code of the react-http-request, and found that there is a weakness that after accepting and sending the second request, the component failed to update the state of "loading" returns.
// starts from Line 49
value: function componentWillReceiveProps(nextProps) {
if (JSON.stringify(this.props) === JSON.stringify(nextProps)) {
return;
}
this.request.abort();
this.performRequest(nextProps);
}
Manually changed the state of loading here can help reset the loading after each request received.
I changed the source code of this lib, and sent the pull request to the repo. It's now merged into master and ejected a new release.
See: https://github.com/mbasso/react-http-request/pull/3
Thus, this problem can be solved by keeping the lib update to the release (currently it is 1.0.3).
I'm trying to do two AJAX calls in my React project and have my UI render according to the data received. This is my render method:
render() {
if (this.state.examsLoaded) {
return (
<div>
<Button onClick={this.openModal}>Details</Button>
<Modal show={this.state.modalOpen} onHide={this.closeModal}>
<Modal.Header closeButton>
<Modal.Title>{this.props.course.name}</Modal.Title>
</Modal.Header>
<Modal.Body>
<DetailModalContent course={this.props.course} exams={this.exams} grades={this.grades}/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeModal}>Sluiten</Button>
</Modal.Footer>
</Modal>
</div>
)
}
else {
return (
<div>Loading...</div>
)
}
}
The render method checks if the AJAX data is available yet and if not, just renders a 'Loading...' message. This is the code that fetches the data:
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
});
});
},
The weird thing is, I used to only have 1 fetch method and everything would work fine. As soon as I called setState the component rerenders and the data is displayed. However, after adding the second one, it doesn't work anymore. See my console.log's. Everything works fine 'till I parse the JSON, after that, nothing gets run anymore.
What am I doing wrong?
Thanks!
fetch's json() method returns a promise. You are using it correctly in the first call, but the second call you are treating it as a function rather than a promise.
Try
gradeResponse.json().then((gradeData) => {
...
});
You need to write this logic inside componentDidUpdate. componentDidMount will be triggered only for the first time.
Please refer to the React documentation.
Probably you will need both componentDidMount and componentDidUpdate.
componentDidMount() {
fetch('http://localhost:8080/course/' + this.props.course.id + '/exams').then((examResp) => {
examResp.json().then((examData) => {
this.exams = examData;
console.log('Course data fetched'); // THIS APPEARS
this.setState({
examsLoaded: true
}); //At this point some state is changed, so componentDidUpdate will be triggered. Then in that function below, grades will be fetched and state is changed, which should call render again.
});
});
},
componentDidUpdate(){
fetch('http://localhost:8080/user/1/grades').then((gradeResponse) => { // THIS DATA IS FETCHED
console.log('Done fetching grades'); // THIS APPEARS
gradeResponse.json((gradeData) => {
console.log('Parsed JSON'); // Here is where it goes wrong. This no longer appears.
this.grades = gradeData;
this.setState({
examsLoaded: true,
modalOpen: false
});
});
});
}
Since I am not with react environment right now. Will update as soon as I try.