Populating a render method using a separate method in ReactJS - javascript

okay so everyone knows a separate mapping will do the trick of rendering a component in a React render method, something like this:
var listOfService = ['A', 'B', 'C', 'D', 'E'];
const listItems = listOfService.map((singleItem) =>
<a className="style_class">{singleItem}</a>
);
and if you call listItems in the return of a render method, you will get the list and thats for a single list but I have a firebase firestore database and I want to loop through that database and get each document printed as a React Component I have called Service, I know that you can't use JSX inside a loop or if statement so I tried this:
renderServices() {
let db = firebase.firestore();
var details = [[]];
db.collection('providers').get().then(function (snapshot) {
snapshot.forEach(function (doc) {
details.push(
[
doc.data().owner_name,
doc.data().experience,
doc.data().charges,
doc.data().address,
doc.data().home_service,
doc.data().highlights,
doc.data().certifications,
doc.data().specialty,
doc.data().facility
]
);
//tried loading here Serivce, didn't work
});
})
.catch(function (error) {
console.log(error);
});
details.map((singleDetail) =>
(< Service details={singleDetail} />)
);
}
so I tried this but its not working, I wrote some console logs and the data is coming in correctly. First I get the whole array of "providers" documents, that providers list gets passed on as "snapshot" so I loop through the snapshot with another function and that function is taking a single document called doc and then pushing it in the var called details, which is an array of arrays but! its not working, the array is not being populated even tho the data is there and therefore I am unable to map, any idea on how to approach this?

You need to separate your loading of data and the rendering of it because retrieving the data is asynchronous and rendering needs to be synchronous.
For example, in your case you are pushing the details into the array in the then callback - this will happen async to your main function so when you call details.map it is still empty (the callback hasn't fired to populate it).
Instead, you can load the data and store it in the state and then render it if it is available. Personally I prefer async/await but the same logic can be achieved with using then on promises:
const [details, setDetails] = useState([]);
// Load the data on initial load or setup the effect to fire when reload is needed
useEffect(() => {
// Async function that loads the data and sets the state once ready
const loadData = async () => {
const details = [];
try {
const snapshot = await db.collection('providers').get();
snapshot.forEach((doc) => {
details.push([
// your details stuff
]);
}
} catch (ex) {
// Handle any loading exceptions
}
setDetails(details);
}
}, [])
// Render the available details, you could also add conditional rendering to check
// if the details are available or not yet
return (
<div>
{details.map((singleDetail) => (
<Service details={singleDetail} />
)};
</div>
);
If you are using React classes instead of functional components, you would call the loading from componentDidMount and still set the state after it is loaded, then map over the state data when rendering.

I have found the solution, using ReactDOM.render method
first make a global array:
var Services = [];
and then add a DOM element where the array can be placed and rendered:
<div className="row d-flex justify-content-center" id='service-div'>
</div>
once you have the DOM element in place, let the page load, when the load completes, ComponentDidMount() is called, so put your method in there so its
ComponentDidMount(){
this.renderServices();}
now in this method, just populate the array and then use ReactDOM.render method to render it to a given DOM element
async renderServices() {
let history = this.props.history; //in case the method can't access the history
let db = firebase.firestore();
selectedServices = this.props.location.state.selectedServices;
var database = firebase.database();
db.collection('providers').where('services', 'array-contains-any',
selectedServices).get()
.then(function (snapshot) {
let idCount = 0;
snapshot.forEach(function (doc) {
let tempDetails = [];
tempDetails.push(
doc.data().owner_name,
doc.data().experience,
doc.data().charges,
doc.data().address,
doc.data().home_service,
doc.data().highlights,
doc.data().certifications,
doc.data().specialty,
doc.data().facility,
doc.data().services,
doc.data().call_extension,
idCount.toString()
);
Services.push(<Service details={tempDetails} history={history} />); //adding the Service Component into a list
idCount++;
});
ReactDOM.render(<div>{Services}</div>, document.getElementById('service-div')); //now rendering the entire list into the div
}).catch(function (error) {
console.log(error);
});
}

Related

Vue & Firebase: Data being duplicated in state unexpectedly

I have two calls to Firebase: one to get the existing data and one to listen for updates in the data. When those updates happen, instead of replacing the existing data for some reason I see to be adding the two datasets together. Can't figure out why as I'm directly updating state with new data in my second function.
Here are the functions called on mounted():
mounted() {
this.getImages();
this.refreshImages();
},
And the two functions in question:
async getImages() {
let snapshot = await db
.collection("Maria")
.orderBy("timestamp", "desc")
.get();
snapshot.forEach((doc) => {
let appData = doc.data();
appData.id = doc.id;
this.picturesData.push(appData);
});
this.dataLoaded = true;
},
async refreshImages() {
await db
.collection("Maria")
.orderBy("timestamp", "desc")
.onSnapshot((snapshot) => {
let newPicturesData = [];
snapshot.forEach((doc) => {
let newPictureData = doc.data();
newPictureData.id = doc.id;
newPicturesData.push(newPictureData);
});
this.picturesData = newPicturesData; // this should overwrite the data in my state, right? But instead it's appending.
});
},
It's difficult to tell you exactly what's happening without thoroughly testing your code but you have to note that the two calls (to getImages() and refreshImages()) may not be done in the order you expect.
Since in getImages() you push the data to picturesData and in refreshImages() you replace picturesData, I suspect that the listener set through refreshImages() returns data before you get the result of the query triggered by getImages().
Actually, since onSnapshot() triggers an initial call that returns the entire result of the query, you only need to call refreshImages() (you don't need the initial call to getImages()).
Note that onSnapshot() is not an asynchronous method like get(), so you don't need to make refreshImages() async.

Cannot setState inside Firestore add promise

At a loss here. I've tried many variations of this but basically I am trying to set the state of uploadRefId to the id returned once I create a new collection. I can see the id is generated via console.log but the setState function does not seem to work within the scope of this promise (the uploadRefId is null)?
const [uploadRefId, setUploadRefId] = React.useState(null);
let uploadRefCol = await firestore.collection("uploads");
await uploadRefCol.add(uploadData)
.then( (uploadDataRef) => {
console.log('uploadDataRef.id: ', uploadDataRef.id);
setUploadRefId(uploadDataRef.id);
console.log('uploadRefId', uploadRefId);
})
.catch( (error) => {
console.log('Error!', error)
});
Changing state can be funny throughout the life cycle of React.
If your uploadDataRef.id is of type string see if this helps:
const [uploadRefId, setUploadRefId] = React.useState('');
If it is a numeric value see if setUploadRefId works with this:
const [uploadRefId, setUploadRefId] = React.useState(0);
This may not be a solution per say but it could lead you to one.
setUploadRefId(uploadDataRef.id);
Above is async update state so if you immediately use state value then the value is yet not set and you are trying to access the value
useEffect(() => {
console.log('uploadRefId', uploadRefId);
})
So above use effect runs on every update in the state you can watch out for any state change
Code sandbox sample

React state is not updating, trying to use ES6 syntax

Why is the state not updating? I have checked that i receive data from the api in the correct form. I can print the result if i change it instead of trying to add it to a new array and put it in the state. I have also checked that this exists in the scope and as I understand it should work since I am using an arrow function. I still receive an empty array after the map function.
What am I missing?
class CurrencyForm extends React.Component {
state = {
currencies: [],
}
componentDidMount(){
this.fetchCurrencies();
}
fetchCurrencies = async () => {
const response = await axios.get('some address')
response.data.map(currency =>
this.setState({currencies: [...this.state.currencies,currency.id]}
))
}
The problem is that you are using [...this.state.currencies, currency.id].
Since setState is also async, the state does not change for each iteration of the map. So you are always using the same this.state.currencies. This means that you should only get the last currency.id added.
You should either use the function version of setState
this.setState((state) => ({currencies: [...state.currencies,currency.id]}));
or simply do the map and use the resulting array to set the state
fetchCurrencies = async () => {
const response = await axios.get('some address');
const currencyIds = response.data.map(currency=>currency.id);
this.setState({currencies: [...this.state.currencies,...currencyIds]}
}
if you want to put all the id's in the state.. instead of calling setState so many times, You could put all the ids in an array first then update the state with that array something like this:
fetchCurrencies = async () => {
const response = await axios.get('some address')
const idsArray = response.data.map(currency => currency.id)
this.setState({currencies: idsArray})
}
and remember setState is an async call so you may not be able to see the result if you put console.log just after setState instead try console logging in your render method

How can I keep a firebase function open when it's in another file

I have a file called db.js from which I make all my firebase calls.
I am calling a function in db.js from another file called home.js
How do I make it that the firebase connection stays open and the data gets passed back to home.js? I can't use a promise because that closes the connection.
Here is the function from db.js:
export function getShopNames() {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").on('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
return stores
})
}
and I call it from home like this:
let stores = db.getShopNames()
I want it to work so if a new store gets added to the real-time database, the variable updates
There is no concept of file based scope in JavaScript. The listener will stay active from the moment you call on('value', until you either call off on that same location or until you load a new page.
But your return stores doesn't do anything meaningful right now. It returns a value from the callback function that nobody will ever see/use.
Data is loaded from Firebase asynchronously, which means you can't return it from a function in the normal way. By the time the return statement runs, the data hasn't loaded yet. That's why you'll usually return a so-called promise, which then resolves when the data has loaded.
In your function that'd be:
export function getShopNames() {
return new Promise(function(resolve, reject) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
resolve(stores);
}, (error) => {
reject(error);
})
}
Now you can call this function like this:
getShopNames().then((shopnames) => {
console.log(shopnames);
})
Update: you commented that you also want to handle updates to the shop names, you can't use once() and can't use promises (since those only resolve once).
Instead pass in a custom callback, and invoke that every time the shop names change:
export function getShopNames(callback) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
callback(stores);
})
}
And then call it like:
getShopnames(function(shopnames) {
console.log(shopnames);
});

FIrebase "pushing" object to an array is not working

app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
ref.on("value", function(snapshot) {
snapshot.forEach((snap) => {
firebase.database().ref('experiences').child(snap.val()).once("value").then((usersnap) => {
recent.push(usersnap.val());
});
});
console.log(recent);
});
res.render('experiences',{key: zone_key, list: recent});
});
In the above code, I am querying a reference point to get a set of "keys". Then for each key, I am querying another reference point to get the object associated to that key. Then for each object returned for those keys, I simply want to push the objects into a list. I then want to pass in this list to the client site to do stuff with the data using the render.
For some reason, the recent [] never gets populated. It remains empty. Is this an issue with my variables not being in scope? I console logged to check what the data the reference points are returning and its all good, I get the data that I want.
P.S is nesting queries like this ok? For loop within another query
As cartant commented: the data is loaded from Firebase asynchronously; by the time you call res.render, the list will still be empty.
An easy way to see this is the 1-2-3 test:
app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
console.log("1");
ref.on("value", function(snapshot) {
console.log("2");
});
console.log("3");
});
When you run this code, it prints:
1
3
2
You probably expected it to print 1,2,3, but since the on("value" loads data asynchronously, that is not the case.
The solution is to move the code that needs access to the data into the callback, where the data is available. In your code you need both the original value and the joined usersnap values, so it requires a bit of work.
app.get('/zones/:id/experiences', function(req,res) {
var zone_key = req.params.id;
var recent = [];
var ref = firebase.database().ref('participants/'+zone_key+'/experiences');
ref.on("value", function(snapshot) {
var promises = [];
snapshot.forEach((snap) => {
promises.push(firebase.database().ref('experiences').child(snap.val()).once("value"));
Promise.all(promises).then((snapshots) => {
snapshots.forEach((usersnap) => {
recent.push(usersnap.val());
});
res.render('experiences',{key: zone_key, list: recent});
});
});
});
});
In this snippet we use Promise.all to wait for all usersnaps to load.

Categories

Resources