I'm trying to fetching my data from mongo to the client-side to react.
I succeed to set the state of the component to the correct fields from the endpoint API of my DB.
But when I want to print my state to see if it is working the console print my an empty object although the state change and I see it in the console.[enter image description here][1]
getDataFromDb = () => {
const req = new Request('http://localhost:5000/family',{
method: 'GET',
cache: 'default'
});
fetch(req).then(res=>{
return res.json();
}).then(data=>{
console.log(data);
this.setState({
rooms: data
});
console.log(this.state.rooms);
}).
catch(err=>{
console("Error: " + err);
});
};
componentDidMount() {
this.getDataFromDb().then(result => this.setState({rooms: result}));
//let rooms = this.formatData(this.getDataFromDb());
//let featuredRooms = ...rooms.filter(room => room.featured===true);
//let maxPrice = Math.max(...rooms.map(item=>item.price));
//let maxSize = Math.max(...rooms.map(item=>item.size));
//new code:
let featuredRooms = this.state.rooms.filter(room=>room.featured===true);
let maxPrice = Math.max(this.state.rooms.map(item => item.price));
let maxSize = Math.max(this.state.rooms.map(item=> item.size));
this.setState({
// old code ---> rooms,
//rooms,
featuredRooms,
sortRooms: this.state.rooms,
//old code
//sortedRooms:rooms,
loading:false,
price:maxPrice,
maxPrice,
maxSize
});
this.printData();
}
In react, setState is an async function. Printing the state (just after changing it) will not give you the latest state. If you want to trigger a function after setting the state you can do the following:
this.setState({
// old code ---> rooms,
//rooms,
featuredRooms,
sortRooms: this.state.rooms,
//old code
//sortedRooms:rooms,
loading:false,
price:maxPrice,
maxPrice,
maxSize
}, () => { this.printData(); });
Related
I tried to create a interactable map following this example here: https://docs.mapbox.com/mapbox-gl-js/example/cluster/
In my componentDidMount (where I create a mapboxgl) I implemented clickable markers, when clicked on the markers a popup appears which displays various informations.
After the click I want to call a second function (fetch) to get more data on that specific marker: this.props.getData(id);
I then want to display these data in the same popup as the other information.
My problem is that this.props.testdata is empty on the first click. If I double-click on the marker, the data appear. So my guess is that my component does not notice the change of the state/prop and therefore does not update?
How do I do that or what am I missing?
Map.js
this.map.on('click', 'unclustered-point', (e) => {
const coordinates = e.features[0].geometry.coordinates.slice();
const id = e.features[0].properties.id;
const infos = e.features[0].properties.infos;
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
if (id == null) {
console.log("Missing id, cant get informations")
return;
}
this.props.getData(id);
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
App.js (getData function):
getData = (id) => {
if (id== null) {
console.log("Missing id")
return;
}
const {mapCenter, startDate, endDate} = this.state;
const neo4j = require('neo4j-driver')
const driver = neo4j.driver('bolt://xxx', neo4j.auth.basic("xx", "xx-xx"))
const session = driver.session()
session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
};
I am not familiar with neo4j, but it is apparent that getData(id) fetches data from a server. This is going to be an asynchronous operation, so you should add a state property to maybe show a spinner while data is being fetched?
Regarding testdata not being available, I do not see the code where it is being set.
Maybe your setState code should be:
this.setState({
testdata: data
});
//If your data prop is testdata.
As per the current setState, data property of your component state would be set with server response.
Updates:
Temporary fix for async server call:
You can change following methods and try if it fixes your issue:
this.map.on('click', 'unclustered-point', async (e) => {
// ...previous code
await this.props.getData(id);
// This forces the following code to execute synchronously. Basically it should wait for your API call to be complete
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(
`
Id: ${id}
<br>
Infos: ${infos}
<br>
<br>
Testdata: ${this.props.testdata}
`
)
.addTo(this.map);
});
this.map.on('mouseenter', 'clusters', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = '';
});
});
getData = (id) => {
//... previous code
// we return a promise to use await in the onClick handler
return session
.run('Here goes a neo4j cypher statment',{id: id})
.then((results)=> {
const data= [];
results.records.forEach((record) => data.push([record.get("r"), record.get("n"), record.get("b")]))
this.setState({
data
});
session.close()
driver.close()
}).catch(e => {
console.log(e)
session.close();
});
}
If you are still facing an issue, please create a sample app and share.
I have not yet managed to fix the original problem.
However, I have found another solution:
In my Map.js I'm calling the this.props.testdata in th UI like this:
<div className="sidebar">
info: {JSON.stringify(this.props.testdata)}
</div>
I am trying to create a Translation app in which I send the text to the api and then i receive the translation back with a text typing effect.
The problem arise when I save the words that I receive to Firebase with debounce function. It always only save the initial part of the translation received (let's say that I get a translation of 100 words, it saves only the first 10) and I can't really figure out why.
This is the function that I use to send the text to the api and then receive the translation back.
handleClick(e) {
e.preventDefault();
let body = {
prompt: this.state.input
};
let thisComponent = this;
axios.post(TRANSLATE_API_URL, body).then(({ data: { text } }) => {
let wordDelay = 100 // Add a word every 100ms
let timerDelay = 0
text.split(" ")
.forEach(word => {
setTimeout(function(){
thisComponent.setState({ input: thisComponent.state.input + " " + word });
},timerDelay);
timerDelay += wordDelay;
});
//Calling Firebase function
this.updateBody(this.state.input);
});
}
And this is the code that I use to save data to Firebase .
updateBody = async (val) => {
await this.setState({ input: val });
await this.setState({ wordsCounted :val.split(' ').length });
await this.setState({charactersCounted : val.length })
/*assign the number of words inside the textarea to the variable wordsCounted*/
this.update();
};
update = debounce(() => {
this.props.noteUpdate(this.state.id, {
body: this.state.input,
})
}, 750);
The only explanation I have given myself is probably a problem with asynch and await.
I'm creating a React Native application. I want to create an image array because I need to upload them to the server. But when I upload multiple images they are not storing like an array. every time the imagesQueue array has only one image.
my code as follows.
const [filePath, setFilePath] = useState({
imagesQueue: []
});
const chooseFile = () => {
const options = {
title: 'Select an option',
storageOptions: {
skipBackup: true,
path: 'images',
},
};
ImagePicker.showImagePicker(options, (response) => {
// console.log('Response = ', response);
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else {
// let source = response;
// You can also display the image using data:
let source = {
uri: 'data:image/jpeg;base64,' + response.data
};
setFilePath({
...filePath,
imagesQueue: [source],
});
console.log("stored item : " + filePath.imagesQueue);
}
});
};
How to store multiple images in the same state. Thanks in advance.
the data probably will store in formData - it's easy format for later passing it to BE.
firstly You need store somewhere those data:
const [selectedFiles, setSelectedFiles] = useState([]);
Later there is possibility to use .concat() - that function connects 2 arrays:
setSelectedFiles(((prevState) => prevState.concat(fileData)));
And in the last step, You need to pass those data to Your form:
const formData = new FormData();
for (let i = 0; i < selectedFiles.length; i++) {
formData.append('Files', selectedFiles[i].File);
}
I am currently having an issue where multiple setStates that use the filtering of an array are interfering with each other. Basically if a user uploads two files, and they complete around the same time, one of the incomplete files may fail to be filtered from the array.
My best guess is that this is happening because they are separately filtering out the one that needs to be filtered, when the second one finishes and goes to filter itself out of the array, it still has the copy of the old incomplete array where the first file has not been filtered out yet. What would be a better way to approach this? Am I missing something obvious? I am thinking of using an object to hold the files instead, but then I would need to create a custom mapping function for the rendering part so that it can still be rendered as if were an array.
fileHandler = (index, event) =>{
let incompleteFiles = this.state.incompleteFiles
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState({ incompleteFiles: incompleteFiles },()=>{
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
let incompleteFiles = this.state.incompleteFiles
let completeFiles = this.state.completeFiles
api.uploadFile(fileData)
.then(res=>{
if(res.data.success){
this.setState(state=>{
let completeFile = {
name : res.data.file.name,
}
completeFiles.push(completeFile)
incompleteFiles = incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
return{
completeFiles,
incompleteFiles
}
})
}
})
})
}
Updated with accepted answer with a minor tweak
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: [
...incompleteFiles.slice(0, index),
{
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
...incompleteFiles.slice(index+1)
],
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
In class components in React, when setting the state which is derived from the current state, you should always pass a "state updater" function instead of just giving it an object of state to update.
// Bad
this.setState({ counter: this.state.counter + 1 });
// Good
this.setState((currentState) => ({ counter: currentState.counter + 1 }));
This ensures that you are getting the most up-to-date version of the state. The fact that this is needed is a side-effect of how React pools state updates under the hood (which makes it more performant).
I think if you were to re-write your code to make use of this pattern, it would be something like this:
fileHandler = (index, event) =>{
this.setState(({ incompleteFiles }) => ({
// Update the state in an immutable way.
incompleteFiles: {
[index]: {
...incompleteFiles[index],
loading: true,
file: event.target.files[0],
},
},
}), () => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
api.uploadFile(fileData)
.then(res => {
if(res.data.success){
this.setState(({ incompleteFiles, completeFiles }) => ({
completeFiles: [
...completeFiles, // Again, avoiding the .push since it mutates the array.
{ // The new file.
name: res.data.file.name,
}
],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name),
})))
}
})
});
}
Another thing to keep in mind is to avoid mutating your state objects. Methods like Array.push will mutate the array in-place, which can cause issues and headaches.
I think change code to this can solve your problem and make code easy to read.
fileHandler = async (index, event) =>{
const incompleteFiles = [...this.state.incompleteFiles]
incompleteFiles[index].loading = true
incompleteFiles[index].file = event.target.files[0]
this.setState(
{
incompleteFiles
},
async (prev) => {
const fileData = new FormData()
fileData.append('file', event.targets[0].file)
const res = await api.uploadFile(fileData)
/// set loading state to false
incompleteFiles[index].loading = false
if (!res.data.success) {
return { ...prev, incompleteFiles }
}
// add new file name into completeFiles and remove uploaded file name from incompleteFiles
return {
...prev,
completeFiles: [...prev.completeFiles, { name : res.data.file.name }],
incompleteFiles: incompleteFiles.filter(inc=>inc.label !== res.data.file.name)
}
})
)
}
so I fetch some data from two urls in order and save the responded data into an array, and then setState to this array. When I console.log(dataArray), I can see the contents, but the console.log(dataArray.length) is 0, no matter I check immediately after setState statement or in render method. what should I do to set state to array before it get rendered again? Thanks!!!
class App extends Component {
state={
dataArray:[]
};
componentDidMount(){
this.getTrainInfo();
}
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station,North+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return data;
})
.forEach(data=>{
fetch('https://api-v3.mbta.com/routes/' + data.trainID)
.then(response2 => response2.json())
.then(res2 => {
data.destination = res2.data.attributes.direction_destinations[0];
arr.push(data);
//setState here work, but set too many times
//this.setState({dataArray:arr})
})
});
})
.then( ()=>{
this.setState({dataArray:arr});
console.log(this.state.dataArray); //log an array full of objects.
console.log(this.state.dataArray.length); //but here length is 0.
})
};
render() {
let data=this.state.dataArray;
let departureTime;
let boardingStatus;
let TrackNo;
let destination;
console.log(data); //log the array full of objects.
console.log(data.length); //but here the length is 0.
if(data.length){
departureTime=new Date(data[0].departureTime).toLocaleString('en-US',{hour:'numeric',minute:'numeric', hour12:true});
boardingStatus= data[0].boardingStaus;
TrackNo=(data[0].trackNo)?data[0].trackNo:0;
destination=data[0].destination;
}
return (
<div className="App">
<h1>Train info</h1>
<h2>{departureTime}
{boardingStatus}
{TrackNo}
{destination}
</h2>
</div>
);
}
}
This part of your code run before your requests is done inside forEach function, so arr can be empty here
then( ()=>{
this.setState({dataArray:arr});
console.log(this.state.dataArray); //log an array full of objects.
console.log(this.state.dataArray.length); //but here length is 0.
})
You can use prmoise.all for this purpose
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station,North+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
let changes = res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return data;
});
let requests = changes.forEach(data=>{
return fetch('https://api-v3.mbta.com/routes/' + data.trainID)
.then(response2 => response2.json())
.then(res2 => {
data.destination = res2.data.attributes.direction_destinations[0];
arr.push(data);
return res2
})
});
Promise.all(requests).then((values) => {
this.setState({dataArray:arr});
});
})
};
Thanks a lot to #aseferov !!!
so turns out I have to use Promise.all() to make sure setState will update all array after all fetches finished.
getTrainInfo=()=>{
let arr=[];
let data={};
fetch('https://api-v3.mbta.com/predictions?filter[stop]=South+Station&filter[direction_id]=0&include=vehicle&sort=departure_time')
.then(response=>response.json())
.then(res=> {
let changes= res.data.map(dat => {
data.departureTime = dat.attributes.departure_time;
data.trackNo=res.data[0].relationships.vehicle.data.id? res.data[0].relationships.vehicle.data.id:0;
data.boardingStaus = dat.attributes.status;
data.trainID = dat.relationships.route.data.id;
return 'https://api-v3.mbta.com/routes/' + data.trainID
});
//let changes=['https://api-v3.mbta.com/routes/CR-Franklin','https://api-v3.mbta.com/routes/Red','https://api-v3.mbta.com/routes/Orange','https://api-v3.mbta.com/routes/Mattapan']
//console.log(changes); //an array of urls
let requests=changes.map(change=>{return fetch(change).then(response=>response.json()).then(res=> res.data)}); //.then(response=>response.json()).then(res=> res.data)
//console.log(requests);
Promise.all(requests)
.then(responses=>responses.forEach(response=>{
console.log(response); //returns Objects: res.data[0], res.data[1],...
let destination=response.attributes.direction_destinations[0];
data.destination=destination;
arr.push(data);
}))
.then(()=>{console.log(arr); this.setState({dataArray:arr}); })
})
};