React - Scoping issue with setInterval - javascript

I have a component that will basically serve as a fully self-contained file being uploaded. For the time being, until I get the upload mechanism in place, I'm just using a timer to simulate a progress change. However, when it hits 100% and tries to send a message to its parent (via statusChange), I've got a scoping issue where this is referring to window. How can I fix this?
The actual error:
Uncaught TypeError: _this.props.statusChange is not a function
componentDidMount() {
this.timer = setInterval(() => {
this.setState({progress: this.state.progress + 5});
if (this.state.progress === 100) {
clearInterval(this.timer);
this.props.statusChange({uploadComplete: true});
}
}, 1000);
debugMode && console.info('[FileUpload] Began uploading %s',
this.props.name);
},
EDIT:
The problem seems to be in the passing of the callback. this.props.statusChange is indeed null the entire time.
Ahhh, damn! It was a scoping issue here. I'll highlight it below:
UploadQueue = React.createClass({
displayName: 'UploadQueue',
propTypes: {
fileList: React.PropTypes.any.isRequired
},
statusChange(status) {
debugMode && console.info('[UploadQueue] Status change! %o', status);
},
render() {
let files = [];
// Convert to a true array
for (let i = 0; i < this.props.fileList.length; ++i) {
files = files.concat(this.props.fileList[i]);
}
return (
<div>
{files.map(function (file) { // should be: {files.map(file => {
return <FileUpload key={file.name}
name={file.name}
statusChange={this.statusChange} />
})}
</div>
)
}
});

this scoping issue was in the component that owns FileUpload. Fixed code below.
{files.map(file => {
return <FileUpload key={file.name}
name={file.name}
statusChange={this.statusChange} />
})}

try this:
componentDidMount() {
this.timer = setInterval(() => {
this.setState({progress: this.state.progress + 5});
if (this.state.progress === 100) {
clearInterval(this.timer);
this.props.statusChange({uploadComplete: true});
}
}.bind(this), 1000);
^^^^^^^^^^^^
debugMode && console.info('[FileUpload] Began uploading %s',
this.props.name);
},

Related

Creating new array vs modifing the same array in react

Following is the piece of code which is working fine, but I have one doubt regarding - const _detail = detail; code inside a map method. Here you can see that I am iterating over an array and modifying the object and then setting it to setState().
Code Block -
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
const { invoiceData } = this.state;
invoiceData.map(invoiceItem => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(detail => {
const _detail = detail;
if (_detail.tagNumber === data.tagNumber) {
_detail.id = data.id;
}
return _detail;
});
}
return invoiceItem;
});
state.invoiceData = invoiceData;
}
this.setState(state);
};
Is this approach ok in React world or I should do something like -
const modifiedInvoiceData = invoiceData.map(invoiceItem => {
......
code
......
})
this.setState({invoiceData: modifiedInvoiceData});
What is the pros and cons of each and which scenario do I need to keep in mind while taking either of one approach ?
You cannot mutate state, instead you can do something like this:
checkInvoiceData = (isUploaded, data) => {
if (isUploaded) {
this.setState({
invoiceData: this.state.invoiceData.map(
(invoiceItem) => {
if (invoiceItem.number === data.savedNumber) {
invoiceItem.details.map(
(detail) =>
detail.tagNumber === data.tagNumber
? { ...detail, id: data.id } //copy detail and set id on copy
: detail //no change, return detail
);
}
return invoiceItem;
}
),
});
}
};
Perhaps try something like this:
checkInvoiceData = (isUploaded, data) => {
// Return early
if (!isUploaded) return
const { invoiceData } = this.state;
const updatedInvoices = invoiceData.map(invoiceItem => {
if (invoiceItem.number !== data.savedNumber) return invoiceItem
const details = invoiceItem.details.map(detail => {
if (detail.tagNumber !== data.tagNumber) return detail
return { ...detail, id: data.id };
});
return { ...invoiceItem, details };
});
this.setState({ invoiceData: updatedInvoices });
};
First, I would suggest returning early rather than nesting conditionals.
Second, make sure you're not mutating state directly (eg no this.state = state).
Third, pass the part of state you want to mutate, not the whole state object, to setState.
Fourth, return a new instance of the object so the object reference updates so React can detect the change of values.
I'm not saying this is the best way to do what you want, but it should point you in a better direction.

How to Run functions synchronously in JavaScript?

I'm new to JavaScript and React and am trying to move away from tutorials so have started making a simple app for my own learning benefit but have run into a roadblock with functions running asynchronously.
In onSearchSubmit, there is a setState which has the following in its callback:
this.findSalaryRangeMin(data.advertiser.id, data.teaser);
this.findSalaryRangeMax(data.advertiser.id, data.teaser);
How can I get these two functions above to run synchronously? findSalaryRangeMax uses this.state.salaryLower which is set in findSalaryRangeMin, but the console.log below reveals that findSalaryRangeMax is firing before findSalaryRangeMin has completed.
findSalaryRangeMax = (advertiserId, teaser) => {
console.log(`this.state.salaryLower: `, this.state.salaryLower);
// ... More code
};
I've read some resources which mention using promises, but I wasn't able to figure out how to apply it... I also am wondering whether it can be achieved with async/await.
Full(ish) Code:
(I've removed some code for simplicity)
import React from "react";
import JobSearch from "../api/jobSearch"; // axios
import SearchBar from "./SearchBar";
class App extends React.Component {
state = {
jobTitle: "",
advertiser: "",
salaryLower: "",
salaryLowerTop: "",
salaryUpper: "",
salaryUpperTop: ""
};
findSalaryRangeMin = (advertiserId, teaser) => {
this.setState({ salaryLower: 0, salaryLowerTop: 200000 }, async () => {
let salaryLowerPrev;
for (var i = 0; i < 20; i++) {
const response = await JobSearch.get(
`http://localhost:3001/salary-range/${advertiserId}/${this.state.salaryLower}/${this.state.salaryLowerTop}/${teaser}`
);
console.log(response);
if (response.data.totalCount === 1) {
salaryLowerPrev = this.state.salaryLowerTop;
this.setState({
salaryLowerTop: Math.round(
(this.state.salaryLowerTop - this.state.salaryLower) / 2 +
this.state.salaryLower
)
});
} else {
this.setState(
{
salaryLowerTop: salaryLowerPrev
},
() => {
this.setState({
salaryLower: Math.round(
(this.state.salaryLowerTop - this.state.salaryLower) / 2 +
this.state.salaryLower
)
});
}
);
}
}
});
};
findSalaryRangeMax = (advertiserId, teaser) => {
console.log(`this.state.salaryLower: `, this.state.salaryLower);
// ... More code
};
onSearchSubmit = async term => {
const response = await JobSearch.get(
`http://localhost:3001/job-info/${term}`
);
if (response.data.totalCount === 1) {
const data = response.data.data[0];
this.setState(
{
jobTitle: data.title,
advertiser: data.advertiser.description
},
() => {
this.findSalaryRangeMin(data.advertiser.id, data.teaser);
this.findSalaryRangeMax(data.advertiser.id, data.teaser);
}
);
} else {
console.log("totalCount not equal to 1: ", response.data.totalCount);
}
};
render() {
return (
<div>
<SearchBar onSearchSubmit={this.onSearchSubmit} />
<hr />
<div>
Job Title: {this.state.jobTitle}
Advertiser: {this.state.advertiser}
Salary Lower Range: {this.state.salaryLower}
Salary Upper Range: {this.state.salaryUpper}
</div>
</div>
);
}
}
export default App;
To give some context, the app I'm trying to make, queries an API for a jobs listing site. The API response doesn't reveal a salary range for an individual job, but the salary can fairly accurately be determined by querying salary ranges.
You are correct in your understanding that async or promises are needed if you want the functions to run synchronously and the existing code will run to the following line findSalaryRangeMax before returning with the data needed.
async/await and promises will definitely help, but often it's worth considering a few code changes too. As an example, you could combine the two functions into a single function like
findSalaryRanges(data.advertiser.id, data.teaser)
and fetch the data, process and set state once.
some pseudo code:
findSalaryRanges = async () => {
// get all the data needed first
const maxSalaryData = await JobSearch.get(`http://localhost:3001/salary-range/${advertiserId}/${this.state.salaryLower}/${this.state.salaryLowerTop}/${teaser}`);
const minSalaryData = await JobSearch.get(...);
// process data as needed
...
// set state once
this.setState({
salaryTop: salaryTop,
salaryLower: salaryLower
});
};
setState is async, so if you are dependent on the value of the state before running the next function you could do something like:
this.setState({
salaryLowerTop: Math.round(
(this.state.salaryLowerTop - this.state.salaryLower) / 2 +
this.state.salaryLower
)
}, () => this.findSalaryRangeMax(data.advertiser.id, data.teaser))
Can I execute a function after setState is finished updating?

Unable to get information from Array

```
function displayResults(responseJson) {
const gamedata = responseJson.results.map(game => {
return {
name: game.name,
consoles: game.platforms,
metacritc: game.metacritic,
genre: game.genres
};
});
console.log(gamedata);
inputData(gamedata);
}
function platformdata(consoles) {
return consoles.map(system => {
return system.platform.name;
});
}
function inputData(gamedata) {
gamedata.map(input => {
$(`#home-list`).html(`
<h1>${input.name}</h1>
<h5>${input.metacritc}</h5>
<span>${input.system}</span>
`);
});
}
```
I have been trying to get information from an array but have not been successful in obtaining the information. The information for the game platforms is somewhat nested and I have been trying to dig it out but to no avail.
https://api.rawg.io/api/games?page_size=1
Best way I can show the information more in detail is to just advise to throw the link above into postman and you'll see what I am trying to work with. Basically it is under results > platforms > platform > name. When I add this information into the map function it comes up undefined. Running it now they come up with saying object with commas. I'd like it to just come up with just the information leaving out the commas. I can't figure out how to get join() to go into html(). Thank you very much!
Edit:
1) Results I'd like is to be able to pull up is within the platforms tree but is buried. If I just use game.platforms it produces [object, Object]. If I try to add more to the line in gamedata it will produce undefined.
2) In "gamedata.map(input => {" ?
3) Yes I tried making a helper function based on code I found online. The code I found online used excessive li and ul
```
function platformnames(platforms) {
return platforms.map(system => {
return '<li>' system.platform.name + '</li>';
});
}
function pullArray(gamedata) {
gamedata.map(function(input) {
let platformNames = input.platforms.map(
system => `<li>${system.platform.name}</li>`
);
$(`#home-container`)
.append(`<li><ul><li>${platformNames}</li></ul></li>`)
.join(' ');
});
}
```
This worked but gave really odd results.
4) No I'm adding it all to the same ID as one pull.
5) That is me trying to mine the information from platforms on an API. It's buried in there and I haven't found a good solution.
function formatParams(params) {
const queryItems = Object.keys(params).map(
key => `${key}=${params[key]}`
);
console.log(queryItems);
return queryItems.join('&');
}
const opts = {
headers: {
'User-Agent': `<ClassProject> / <VER 0.01> <Currently in Alpha testing>`
}
};
function fetchAPI() {
const params = {
...($('.search-param').val() && {
search: $('.search-param').val()
}),
...($('.genre-param').val() && {
genres: $('.genre-param').val()
}),
...($('.platforms-param').val() && {
platforms: $('.platforms-param').val()
}),
...($('.publishers-param').val() && {
publishers: $('.publishers-param').val()
}),
page_size: '1'
};
console.log(params);
const baseURL = 'https://api.rawg.io/api/games';
const queryString = formatParams(params);
let url = `${baseURL}?${queryString}`;
console.log(url);
fetch(`${url}`, opts)
.then(response => response.json())
.then(responseJson => displayResults(responseJson))
.catch(error => {
console.log(`Something went wrong: ${error.message}`);
});
}
function displayResults(responseJson) {
const gamedata = responseJson.results.map(game => {
return {
name: game.name,
consoles: game.platforms,
metacritc: game.metacritic,
genre: game.genres
};
});
console.log(gamedata);
inputData(gamedata);
}
function inputData(gamedata) {
let html = '';
gamedata.forEach(input => {
html += `<h1>${input.name}</h1>`;
html += `<h5>Metacritic: ${input.metacritic ||
'No metacritic rating'}</h5>`;
html += 'Platforms:<br />';
input.consoles.forEach(e => {
html += `<span>${e.platform.name}</span><br />`;
});
html += `<br /><span>System: ${input.system}</span>`;
});
document.getElementById('home-list').innerHTML = html;
}
function pageLoad() {
$(document).ready(function() {
fetchAPI();
});
}
pageLoad();
So I'm close thanks to the help of everyone here. Now I'm returning "Metacritic: No metacritic rating" or if I remove that or part an undefined. What am I missing?
The snippet below gets you the platform names. I modified/created
the displayResults() function to only return a value (and also corrected the typo in metacritic (metacritc -> metacritic))
the inputData() function to create a correct HTML and append it to the container
a fetchData() function to actually fetch the data
an unnamed function to initiate fetch and display the data
You should look at your data - you don't use game.genres (although you map it) and you would like to display input.system that is not mapped.
function displayResults(responseJson) {
return responseJson.results.map(game => {
return {
name: game.name,
consoles: game.platforms,
metacritic: game.metacritic,
genre: game.genres
};
});
}
function platformdata(consoles) {
return consoles.map(system => {
return system.platform.name;
});
}
function inputData(gamedata) {
let html = ''
gamedata.forEach(input => {
html += `<h1>${input.name}</h1>`
html += `<h5>Metacritic: ${input.metacritic || 'No metacritic rating'}</h5>`
html += 'Platforms:<br />'
input.consoles.forEach(e => {
html += `<span>${e.platform.name}</span><br />`
})
html += `<br /><span>System: ${input.system}</span>`
});
document.getElementById('home-list').innerHTML = html
}
async function fetchData() {
const data = await fetch('https://api.rawg.io/api/games?page_size=5')
const json = await data.json()
return json
}
(async function() {
const json = await fetchData()
inputData(displayResults(json))
})();
<div id="home-list"></div>
And although it does work - you're not supposed to use more than one h1 tag on a site - it will be an HTML validation warning (SEO!). If you will display only one game per page, then forget my remark :)

Vue js Laravel - Axios making Multiple ( 2 ) Requests when changing pages , Category and Subcategory

I'm building an application with Vue on the frontend and Laravel PHP on the backend. Its a single page app (SPA).
When changing pages, sometimes - not always - axios makes two requests for the same page. I'm having trouble figure it out what is happening.
When the link changes I have two watchers, one for the top category and another for the sub-category. They trigger the created () hook that calls the loadData method.
If I change the main category and sub category ( Example: from 1/5 to 2/31 ), the loadData method is called two time. How can I fix this ?
Google Network Tab (The .json request does not represent the same type of page that I'm referring above, only the numbers ) :
<script>
import axios from 'axios'
import { mapGetters } from 'vuex'
import Form from 'vform'
export default {
data() {
return {
products: {
cat : {} ,
products : []
},
productsShow: '',
quickSearchQuery: '',
loadeddata : false,
}
},
methods: {
loadMore () {
this.productsShow += 21
},
loadData () {
if ( this.$route.params.sub_id ) {
axios.get('/api/cat/' + this.$route.params.cat_id + '/' + this.$route.params.sub_id).then(response => {
this.products = response.data
this.productsShow = 21
this.loadeddata = true
}).catch(error => {
if (error.response.status === 404) {
this.$router.push('/404');
}
});
} else {
axios.get('/api/cat/' + this.$route.params.cat_id ).then(response => {
this.products = response.data
this.productsShow = 21
this.loadeddata = true
}).catch(error => {
if (error.response.status === 404) {
this.$router.push('/404');
}
});
}
},
computed: {
...mapGetters({locale: 'lang/locale', locales: 'lang/locales' , user: 'auth/user'}),
filteredRecords () {
let data = this.products.products
data = data.filter( ( row ) => {
return Object.keys( row ).some( ( key ) => {
return String( row[key] ).toLowerCase().indexOf(this.quickSearchQuery.toLowerCase()) > -1
})
})
return data
}
},
created() {
this.loadData()
},
watch: {
'$route.params.cat_id': function (cat_id) {
this.quickSearchQuery = ''
this.loadData()
},
'$route.params.sub_id': function (sub_id) {
this.quickSearchQuery = ''
this.loadData()
}
}
}
</script>
So I see that you figured the issue by yourself.
To fix that, you can delay the loadData function (kind of debounce on the tail) so if it's being called multiple times in less than X (300ms?) then only the last execution will run.
try:
loadData () {
clearTimeout(this.loadDataTimeout);
this.loadDataTimeout = setTimeout(()=> {
//The rest of the loadData function should be placed here
},300);
}
Resolved :
watch: {
'$route.params': function (cat_id, sub_id) {
this.quickSearchQuery = ''
this.loadData()
},

How to make sure the function is executed in VueJS

I'm trying to execute 3 functions, and after than console.log the values that they change. I think there should be better approach for this kind of problems, but I'm not sure what it is. What I've done is I went old school, and added loading flag. Basically, loading = 3, when function is loaded, loading--
I'd like to demonstrate my current code (well actually it's not the same, but it will work for demo purposes), so you can get the feeling:
data:() => ({
loading: 3,
first: null,
second: null,
third: null
}),
methods: {
first() {
this.$route.get('/data/for/first').then(response => {
this.first = response.data;
this.loading--;
})
},
second() {
this.$route.get('/data/for/second').then(response => {
this.second = response.data;
this.loading--;
})
},
third() {
this.$route.get('/data/for/third/a').then(responseA => {
let thirdA = responseA.data;
this.$route.get('/data/for/third/b').then(responseB => {
let thirdB = responseB.data;
if (thirdA === thirdB) {
this.third = true;
}
this.loading--;
})
})
},
fireFunctions() {
this.first();
this.second();
this.third();
}
},
watch: {
loading: function() {
if (this.loading === 0) {
console.log(this.first, this.second, this.third)
}
}
}
The output looks like this:
dataForFirst, dataForSecond, dataForThird;
But, if I don't use the watcher, and load this.fireFunctions() in mounted() i get:
dataForFirst, dataForSecond, undefined;
Now, as I understand, this is happening because this.third() needs more time to process the data. As you can see in the code, I added loading flag. So, fire functions will only execute when all of the functions are loaded.
I don't think this is the best approach, so I'd like to hear your opinion on this one.
How would you handle it?
Use Promise.all to wait on all your async functions to return and then run whatever code you need to afterward, example:
methods: {
async all() {
let [first, second, third] = await Promise.all([
this.$route.get('/data/for/first'),
this.$route.get('/data/for/second'),
this.$route.get('/data/for/third')
]);
this.first = first;
this.second = second;
this.third = third;
console.log(first, second, third);
}
}

Categories

Resources