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 });
});
Related
This has been beating me up.
I have a vuex store, inside is a folder called "RyansBag" im using to test things.
I have two other folder, Alerts and Inventory. So the folder structure for each of these goes
store> Ryansbag/Alert/...
in my Inventory index.js file, we run a function to add an item to an inventory system.
async addInventory_Catalog({commit}, payload){
try{
const response = await this.$axios.put('Inventory/AddFromCatalogDefault', null, {
params:{
originalUPC: payload.upc,
clientID: payload.clientId,
saleprice: payload.sellPrice,
cost: payload.sellPrice,
Condition: payload.condition.conditionName,
Serial: payload.serialNumber,
Notes: payload.notes,
HoldDays: payload.holdDays,
}
});
console.log(response.data.success)
commit('RyansBag/Alerts/showAlerts', 'You have added a product!', {root: true})
return response.data;
} catch (error) { alert(error); console.log(error); }
},
Here we just pass the item down, and when it's done - commit the changes to our alert which is in store > RyansBag/Alerts.
You can see I tried to call it:
commit('RyansBag/Alerts/showAlerts', 'You have added a product!', {root: true})
My understanding was to simply state the commit is coming from this store as the root state...? But Im not sure if im supposed to register the commit in /Alerts as a global item somehow. ( https://vuex.vuejs.org/guide/modules.html#accessing-global-assets-in-namespaced-modules )
EDIT:::
Edit: There was no commit request in the action. added to post. Now however I get warning to not mutate state outside of state handlers..
Below is the mutation it's requesting to reach inside the alerts.
export const mutations = {
showAlerts(state, message) {
let timeout = 0
if (state.status.showAlert) {
state.status.showAlert = false
timeout = 300
}
setTimeout(() => {
state.status.showAlert = true
state.status.message = message
}, timeout)
},
hideAlerts(state) {
state.status.showAlert = false
},
}
The fix was a) making sure I called commit in the action parameters. and b) not using setTimeout in the mutation, but instead setting a mutation to hide, and using the actions to setTime.
I have one query and one subscription, what I am trying to do is add my data to previous query so that it shows the full list.
I have one query which is returning me list of students and I am rendering that on UI like below
function Test(props) {
const { loading, data: dta } = useQuery(GETSTUDENTS);
const { data: d } = useSubscription(GETSUBSTUDENTS, {
onSubscriptionData: ({ subscriptionData: { data } }) => {
let fname = data.getSubStudent.fname;
let lname = data.getSubStudent.lname;
dta.getStudents.push({ fname, lname });
},
});
return (
<div className="">
{dta &&
dta.getStudents.map((li) => {
<div>
<p>{li.fname}</p>
<p>{li.lname}</p>
</div>;
})}
</div>
);
}
export default Test;
But the main issue is the above one is not updating the cache so when I change the routes and come bqack again it takes the previous data only.
So What I wnat to know na what is the best way to do this, I have check subscribeToMore also but did not get idea How to implement that and how it works with hooks.
I am getting some data from subscription and on that basis I want to change some other part so can I use refetchQueries I did not found any good tutorial which uses hooks (react-apollo-hooks) using qraphql
First, you can just use the pooling option of the useQuery instead of subscription,
I suggest you check it.
From Apollo docs:
"In the majority of cases, your client should not use subscriptions to
stay up to date with your backend. Instead, you should poll
intermittently with queries, or re-execute queries on demand when a
user performs a relevant action."
Apollo subscription
If you still want to use the subscription I think you should use the subscribeToMore and to update your cache policy inside the apollo cache file:
const cache = new InMemoryCache({
typePolicies: {
Agenda: {
fields: {
tasks: {
merge(existing = [], incoming: any[]) {
return [...existing, ...incoming];
},
},
},
},
},
});
You can read more about it here: merge cahce
And check that video: youtube apollo cache
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))
}
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).