Firebase, React: Changing a GET request from get() method to onSnapshot() method - javascript

I've written a function that gets data from my firestore, filters through it, and returns data posted only by the logged-in user. The data, in this case, are gig listings:
authListener() {
auth().onAuthStateChanged((user) => {
if (user) {
this.setState(
{
userDetails: user
},
() =>
axios
.get(
"https://us-central1-gig-fort.cloudfunctions.net/api/getGigListings"
)
.then((res) => {
let filteredGigs = res.data.filter((gig) => {
return gig.user === this.state.userDetails.uid;
});
this.setState({
filterGigs: filteredGigs
});
})
);
} else {
this.setState({
userDetails: null,
});
console.log("no user signed in");
}
});
}
However, when I render this data, I want it to be subscribed in real time to database changes, and I also want to be able to access the document id. In order to do this, I want to change this function to use the onSnapshot() method, here's my (rough) attempt:
authListener() {
auth().onAuthStateChanged((user) => {
if (user) {
this.setState(
{
userDetails: user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot=> {
let filteredGigs = querySnapshot.filter(gig => {
return gig.user === this.state.userDetails.uid;
})
})
this.setState({
filterGigs: filteredGigs
});
})
);
This code isn't working and I have a feeling there's more than one issue, but the main error I'm getting back is querySnapshot.filter is not a function. Can anyone suggest how to re-configure my code so I can use the snapshot method, instead of my axios get request?

querySnapshot is a QuerySnapshot object. As you can see from the API documentation, there is no method filter on that object. If you want something to filter, perhaps you meant to use its docs property, which is an array of QueryDocumentSnapshot objects that contain the actual document data. So, maybe something more like this is what you want:
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid;
})

If you take aa look at the docs, the onSnapshot() method returns a QuerySnapshot. The QuerySnapshot has a docs property which has all the documents in the snapshot. So you can filter them like this :
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => querySnapshot.docs.filter(d => {
const data = d.data()
// Do whatever with data
})

Related

Synchronously populate/modify an array and return the modified array inside a promise

I'm pretty new to ReactJS and redux, so I've never really had to work with this before. I'm retrieving data from an API call in my project. I want to modify the data by adding a new property to the object. However, because the code is not ran synchronously, the unmodified array is being returned (I assume) instead of the modified array.
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
if(things) {
things.forEach((thing, thingIndex) => {
things[thingIndex].items = []
if (thing.hasOwnProperty('channels') && thing.channels) {
thing.channels.forEach(channel => {
if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
channel.linkedItems.forEach(itemName => {
dispatch(loadItems(itemName)).then(
item => things[thingIndex].items.push(item) // push the items inside the "things"
)
})
}
})
}
})
}
things.forEach(data => console.log(data.items.length, data.items)) // data.items.length returns 0, data.items returns a populated array
return things // return the modified array
}).catch(error => {
//todo: handle error
return false
})
}
}
As you can see, I perform an API call which returns data named response. The array is populated with all "things". If things exists, I want to load extra information named "items". Based on the information in the things array, I will perform another API call (which is done by dispatching the loadItems function) which returns another promise. Based on the data in the results of that API call, I will push into the items property (which is an array) of the things object.
As you can see in the comments, if I loop through the things array and log the items property which I just created, it's basically returning 0 as length, which means the things array is being returned before the things array is being modified.
I would like to know two things:
What is causing my code to run async. Is it the
dispatch(loadItems(itemName)) function since it returns a promise?
How am I able to synchronously execute my code?
Please note: this function loadThings() also returns a promise (if you're not familair with redux).
You might be interested in knowing what I tried myself to fix the code
Since I fail to understand the logic why the code is ran async, I've been trying hopeless stuff. Such as wrapping the code in another Promise.all and return the modified array in that promise. I used the then method of that promise to modify the things array, which had the exact same result. Probably because return things is being executed outside of that promise.
I'd really love to know what is going on
Edit
I have added the loadItems() code to the question, as requested:
export function loadItems(itemName) {
return dispatch => {
const url = itemName ? API_GET_ITEMS_ENDPOINT + `/${itemName}` : API_GET_ITEMS_ENDPOINT;
return dispatch({
type: 'LOAD_ITEMS',
payload: {
request: {
url: url,
method: 'GET'
}
}
}).then(response => {
return response.payload.data
})
}
}
My approach would be to map over things, creating arrays of promises for all of their items wrapped in a Promise.all, which gives you an array of Promise.all's.
Then you return things and this array of promises in another Promise.all and in the next then block, you can just assign the arrays to each thing with a simple for loop:
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
const items = things.map((thing) => {
const thingItems = []
if (thing.hasOwnProperty('channels') && thing.channels) {
thing.channels.forEach(channel => {
if (channel.hasOwnProperty('linkedItems') && channel.linkedItems) {
channel.linkedItems.forEach(itemName => {
thingItems.push(dispatch(loadItems(itemName)));
});
}
});
}
return Promise.all(thingItems);
});
return Promise.all([things, Promise.all(items)])
})
.then(([things, thingItems]) => {
things.forEach((thing, index) => {
thing.items = thingItems[index];
})
return things;
})
.catch(error => {
//todo: handle error
return false
})
}
}
Edit:
You need to push the dispatch(loadItmes(itemName)) calls directly into thingItems.
I guess you could refactor it like the following:
export function loadThings() {
return dispatch => {
return dispatch({
type: 'LOAD_THINGS',
payload: {
request: {
url: API_GET_THINGS_ENDPOINT,
method: 'GET'
}
}
}).then(response => {
let things = response.payload.data;
// Retrieve all items for the loaded "things"
if( things ) {
return Promise.all( things.reduce( (promises, thing) => {
if (thing.channels) {
thing.items = [];
promises.push( ...thing.channels.map( channel =>
channel.linkedItems &&
channel.linkedItems.map( item =>
loadItems(item).then( result => thing.items.push( result ) )
) ).flat().filter( i => !!i ) );
}
return promises;
}, []) );
}
return things;
}).catch(error => {
//todo: handle error
return false
})
}
}
In case you would have things, it would check for the channels and the linkedItems for that channel, and create a promise that will push the result back to the thing.items array.
By returning the Promise.all, the continuation of the loadThings would only complete when the Promise.all was resolved. In case there are no things, just things gets returned (which would be a falsy value, so I am wondering how valid that statement could be)
I haven't actually tested the refactoring so there might be some brackets in need of adjusting to your situation, but I guess it gives you an idea?

NodeJS, ReactJS: Axios post not updating in time

I have a really strange issue with an axios post request. In a onSubmit function on a react component I have an axios post that sends an UID and retrieves the number of items with that UID in a collection. In my case is items in the basket for the user. I want to be able to then set that number of items to the component state to use it later for validation. For some reason when I call the request and I print the res.data I get the right output which is the number of items currently in the basket for that UID. When I call with setState however it first doesn't change the state. And as I add items in the basket, it gives me the value that I had previously.
axios.post('http://localhost:5000/basket/check', {uid : UID})
.then(res => this.setState({basket: res.data}))
.catch((error) => {
console.log(error);
})
console.log(this.state.basket);
if (this.state.basket < 4) {
this.props.addBasketItem(basketItem);
}
That is my code. For example I set the basket item in the state 0. When the function gets called first two times the console.log(this.state.basket); prints 0. Then 1,2,3,4 as items get added. But the number of items in the basket is always 2 more which is really strange. What am I doing wrong?
Since setting of state is async you should use callback function in after setting state in order to be sure that only after state is set, you can use it with the freshest data. Try this...
checkBasket = () => {
const { basket } = this.state;
if (basket < 4) {
this.props.addBasketItem(basketItem);
}
}
axios.post('http://localhost:5000/basket/check', {uid : UID})
.then(res => this.setState({basket: res.data}, () => this.checkBasket())
.catch((error) => {
console.log(error);
});
As I understand you want to access the state right after you set it, we can accomplish this with setState callback.
submitHandler = () => {
axios
.post("http://localhost:5000/basket/check", { uid: UID })
.then(res =>
this.setState({ basket: res.data }, () => {
console.log(this.state.basket);
if (this.state.basket < 4) {
this.props.addBasketItem(basketItem);
}
})
)
.catch(error => {
console.log(error);
});
};
A better solution to this would be using componentDidUpdate lifecycle.
submitHandler = () => {
axios
.post("http://localhost:5000/basket/check", { uid: UID })
.then(res => this.setState({ basket: res.data }))
.catch(error => {
console.log(error);
});
};
componentDidUpdate(prevProps, prevState) {
console.log(this.state.basket);
if (this.state.basket < 4) {
this.props.addBasketItem(basketItem);
}
}

How do I iterate through the results of a firebase get

I am trying to all the users from a firebase doc. I suspect my problem is a limitation with my understanding with javascript though.
I've tried this with and without the async/dispatch pattern with no luck
const getUsers = () => {
database.collection('users').onSnapshot(docs => {
const users = [];
docs.forEach(doc => {
users.push({
...doc.data(),
id: doc.id,
ref: doc.ref,
});
});
return users;
});
};
let users = getUsers();
users &&
users.map(user => {
console.log('name: ', user.displayName);
});
Ultimately, I'd like to loop through each user
As explained by #GazihanAlankus, since the query to the Firestore database is an asynchronous operation, you need to return a Promise in your function, by using the get() method, as follows:
const getUsers = () => {
return //Note the return here
database
.collection('users')
.get()
.then(querySnapshot => {
const users = [];
querySnapshot.forEach(doc => {
users.push({
...doc.data(),
id: doc.id,
ref: doc.ref
});
});
return users;
});
};
Then you need to call it as follows:
getUsers().then(usersArray => {
console.log(usersArray);
usersArray.map(user => {
console.log('name: ', user.displayName);
});
});
because the function returns a Promise, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then.
You can do the following test and see in which order the console.log()s are executed and what the second console.log() prints in the console:
getUsers().then(usersArray => {
console.log(usersArray);
usersArray.map(user => {
console.log('name: ', user.name);
});
});
console.log(getUsers());
You can't have a non-async function that gets users from Firebase. You're trying to make impossible happen.
Here's what's wrong with your code: the return users; is returning from the closure that you give to onSnapshot. It's not returning from getUsers. There is no return in getUsers, so it actually returns undefined.
The closure you give to onSnapshot runs in the future but getUsers has to return in the past. There is no way to pass that data in the past when you don't have it. You have to return a promise and deal with it in the calling code. You cannot write a getUsers that directly returns the users instantly.

Use fetch results to make another fetch request (JS/React)

What's the best approach to using the results of one fetch request to make another fetch request to a different endpoint? How can I confirm the first fetch has completed and setState has happened?
class ProductAvailability extends React.Component {
state = {
store_ids: []
}
componentDidMount() {
fetch(`myapi.com/availability?productid=12345`)
.then((results) => {
return results.json();
})
.then((data) => {
const store_ids = data.result.map((store) => {
return store.store_id
})
this.setState({store_ids: store_ids})
})
/* At this point I would like to make another request
to myapi.com/storedata endpoint to obtain information
on each store in the state.store_ids, and add details
to the state */
}
render() {
return (
<div>
<p>STORE INFO FROM STATE GOES HERE</p>
</div>
)
}
}
When you do setState, it updates the component, so the natural point to read state after you've done a setstate, if you'd have read the docs, is in componentDidUpdate(prevProps, prevState).
I leave you to the doc: https://reactjs.org/docs/react-component.html#componentdidupdate
Attention: Don't use willupdate, it's unsafe as you read in the docs.
A further consideration could be done. If you could avoid to put these data in the state, you could also do everything in the componendDidMount (with promiseall for all the other requests maybe) and then set the state with the old and new data, this is preferable since you update your component only once.
Easier to do with async/await (.then/.catch requires a bit more work -- also, reference to Bluebird's Promise.each function). For clarity, its best to move this out of componentDidMount and into its own class method.
As a side note, if this action is happening for every product, then this secondary query should be handled on the backend. That way, you only need to make one AJAX request and retrieve everything you need in one response.
componentDidMount = () => this.fetchData();
fetchData = async () => {
try {
const productRes = fetch(`myapi.com/availability?productid=12345`) // get product data
const productData = await productRes.json(); // convert productRes to JSON
const storeIDs = productData.map(({store_id}) => (store_id)); // map over productData JSON for "store_ids" and store them into storeIDs
const storeData = [];
await Promise.each(storeIDs, async id => { // loop over "store_ids" inside storeIDs
try {
const storeRes = await fetch(`myapi.com/store?id=${id}`); // fetch data by "id"
const storeJSON = await storeRes.json(); // convert storeRes to JSON
storeData.push(storeJSON); // push storeJSON into the storeData array, then loop back to the top until all ids have been fetched
} catch(err) { console.error(err) }
});
this.setState({ productData, storeData }) // set both results to state
} catch (err) {
console.error(err);
}
}

Asynchronous ActionCreator in React Redux

I'm pretty new in React-Redux. Was working on an application. The thing is that I faced some issues with asynchronous execution of Redux actionCreator, may be.
Below is my component. Say, I want to call an actionCreator from componentDidMount() or from an onclick event listener.
class Dashboard extends PureComponent {
componentDidMount() {
this.props.getProductsAndPackages();
let something = [];
something = this.props.products;
}
....................................
}
Or , the function this.props.getProductsAndPackages(); can be an onClick event handler that does the same thing, context is the same. I'll ask my question after first explaining my code.
At the lower side of my Dashboard container:
Dashboard.propTypes = {
getProductsAndPackages: PropTypes.func.isRequired,
products: PropTypes.array.isRequired,
.......................
};
const mapStateToProps = (state) => {
return {
.....................
products: state.products.products,
...................
};
};
const mapDispatchToProps = (dispatch) => {
return {
getProductsAndPackages: () => dispatch(getProductsAndPackagesActionCreator()),
};
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Dashboard));
My actionCreator goes like:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
const local_token = localStorage.getItem('_token');
const fullToken = 'Bearer '.concat(local_token);
axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
});
} else {
axios.get(url)
.then(response => {
dispatch(productsIsLoading(false));
if (response.data.statusCode === 200) {
dispatch(productsFetched(true));
dispatch(products(response.data.data));
} else {
dispatch(productsFetched(false));
dispatch(productsErrors(response.data.message));
}
})
.catch(error => {
console.log(error);
dispatch(productsIsLoading(false));
dispatch(productsErrors(error.message));
});
}
};
};
Now, I want my getProductsAndPackagesActionCreator() to return a Promise or anything that would allow my something variable to get the actual data returned from the server. Right now, by the time I'm getting actual data, the line something=this.props.products has already been executed and I get back the initialValue that was set for products.
I know, whenever I'll receive the populated products, component will re-render, but that does not help my decision making.
I'm using redux-thunk, by the way.
What should I do now ? Sorry for such a long post.
Actually I wanted getProductsAndPackagesActionCreator() to return a promise, which was pretty straightforward, to be honest. I figured out that if you just return the axios.get() or axios.post(), it will return a promise. So, the modified code looked like below:
export const getProductsAndPackagesActionCreator = () => {
return (dispatch) => {
dispatch(productsIsLoading(true));
let url = 'xyz';
if(!!localStorage.getItem('_token')) {
return axios.get(url, {headers: {Authorization: fullToken}})
.then(response => {
............
............
})
.catch(error => {
});
} else {
return axios.get(url)
.then(response => {
...........
...........
})
.catch(error => {
console.log(error);
});
}
};
};
And then, I could do something like below in componentDidMount() or on any onClick event:
this.props.getProductsAndPackages().then(() => {
this.setState({
...this.state,
clicked_product: this.props.product_by_id
}, () => {
//do other stuffs
});
});
Feel free to let me know if there's any issue.
I think you are close to getting what you want. First of all, you should understand that redux actions and react actions like setState are asynchronous, so you have to apply your logic keeping this in mind. I'm going to explain what i think in some points:
You have called the action creator in the correct place componentDidMount, also you can call this action in any onClick if you want.
As soon as you dispatch the action you are changing your redux state setting loading true I suppose. So now you can access this property in your render function, so you can render a Loader until your api call finishes.
When your ajax function finishes, with an error or not, I suppose you are setting loading to false and updating your products data, so you can render now your loaded products in your dashboard.
Are you sure that you have to compare your empty products array with the received data? Maybe you can check in your render function if (!this.props.products.length) return null, when you load your page you will see a loader function and later your dashboard with the products.
If you really need to compare previous products with received products componentDidUpdate is your method. In this method, you can access your previous props and compare with actual props, be careful comparing arrays, remember [] === [] is false. Maybe you can compare the length, something like
componentDidUpdate(prevProps){
if(prevProps.products.length !=== this.props.products.lenth){
doSomething()
}
}
Just to say that componentDidUpdate is executed after render, so be careful with your code to no-execute extra renderings.
Hope it helps, if you dont understand anyting just tell me :)

Categories

Resources