I have this Redux action, which looks up a user and returns all the items they have:
export const itemsFetch = () => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items`)
.on('value', snapshot => {
dispatch({ type: ITEMS_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
Works great, and each item returned has a unique key associated with it.
I want to modify this action to look up specific items, which I've done. That works fine too:
export const itemLookup = uid => {
const { currentUser } = firebase.auth();
return dispatch => {
firebase
.database()
.ref(`users/${currentUser.uid}/items/${uid}`)
.on('value', snapshot => {
dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: snapshot.val() });
});
};
};
This also works fine, but I can only use this to lookup a single item.
I want to loop over an array of item ids, and lookup details for each. Doing this from a component, and using mapStateToProps, causes the component to rerender each time, losing the previous lookup in the process.
Is it best to loop over the ids I have at a component level, and make multiple calls. Or should I pass the array to the action, and somehow loop over them within the action?
Thanks
I feel like I'm doing something dumb, or misunderstanding Redux completely.
In my opinion, this is one of the few limitations that firebase has (along side with queries) that sometimes make me want to grow hair again and lose it (I am bald).
I am more experienced with Firestore although I have used Database, but I think you are correct that you can only request one item in Firebase. What I would do to solve this, is to create a new action that receives an array of IDs and then executes and array of promises that will query each doc.
Something like (pseudo code, and you might need to wrap your firebase call into a promise):
let promises = [];
arrayIds.forEach(id => {
promises.push(firebase.database().ref.get(id))
})
return Promise.all(promises).then(dispatch(results))
Now, if you find that the amount of results are usually not a lot, it is totally fine (and usually the way Firebase requires you to) to complete the data filtering in the client.
Using the response from sfratini, I managed to work it out. Here's the action:
export const itemLookup = oid => {
const { currentUser } = firebase.auth();
const items = []
return dispatch => {
new Promise(function (res, rej) {
oid.forEach(id => {
firebase.database().ref(`users/${currentUser.uid}/items/${id}`).on('value', snapshot => {
items.push(snapshot.val())
})
})
}).then(dispatch({ type: ITEM_LOOKUP_SUCCESS, payload: items }))
}
I used the same reducer, and now the items array makes it down to component level. Perfect!
Related
I use React with Redux and Firebase. Here is one of the functions from my Action.js
export const loadItemsInCategory = (categoryId) => {
return (dispatch) => {
let itemsArray = [];
firestoreService.getItemsInCategory(categoryId)
.then(updatedGroceryList => {
itemsArray = updatedGroceryList;
console.log(`category id is ${categoryId}`)
dispatch(loadItemsInCategoryHelper(categoryId, itemsArray))
})
.catch((error) => console.log(error));
}
}
It's a normal FireStore query. Here is what happens in firestoreService.getItemsInCategory(categoryId)
export const getItemsInCategory = async (categoryId) => {
console.log(`firebase category id is ${categoryId}`)
const snapshot = await db.collection('Item').where('category', '==', categoryId).get()
return snapshot.docs.map(doc => {console.log("called");return {id: doc.id, ...doc.data()}});
}
Right now, my application shows the list of items in the given Category. However, the list does not get updated when a new Item is added to the category by someone else. In other words, additions in FireStore collection does not reflect on my screen unless I refresh the page.
How can I code my webapp in such a way that any change on the FireStore end gets reflected on my webapp?
Thanks in advance!
Your code is doing a one-time query with get(). Queries made like this are not realtime. They don't refresh.
If you want to receive updates to your query in realtime, you should follow the documentation for realtime queries. Instead of using get(), you will use onSnapshot(). And instead of getting a promise, you will attach a listener callback that will be invoked whenever there is a change in the results of the query. Because of these differences, your code will look drastically different.
Objective
I am trying to pass some objects I get back from Firestore into my reducer so I can display some results back to the user.
Problem
When I try to call and pass the query to the reducer it does not appear to work. I am running a console.log to see if the reducer gets called but nothin it appearing. I think is because I have nested return statements?, Is this true?
Here is my action:
export const queryBidder = (value) => {
return async (dispatch, getState, { getFirestore }) => {
const firestore = getFirestore();
const normalizeValue = _.capitalize(value);
let Query = []
firestore.collection("bidders").where("firstname", ">=", normalizeValue).get()
.then(snapshot => {
if (snapshot.empty) {
console.log('No matching bidders.');
return;
}
snapshot.forEach(doc => {
Query.push(doc.data());
return { type: actionTypes.QUERYBIDDER, Query: Query };
});
})
.catch(err => {
console.log('Error getting documents', err);
});
}
};
When I put the return statement above the async return statement, all works well. However, I need to be able to call getFirestore(). This setup comes from a tutorial I found on Udemy, to be honest I don't quite understand it.
return async (dispatch, getState, { getFirestore })
How are these getting passed in? Where do they come from? Why does the order matter? How can I just access the getfirestore() function without wrapping my actions logic in this function?
I am unsure of why this worked but while awaiting for responses I opted to change from return to dispatch.
dispatch({ type: actionTypes.QUERYBIDDER, Query: Query });
this resolved my issue. Look for a more thorough answer of why this worked and my above original questions.
From redux documentation about using dispatch - This is the only way to trigger a state change. So basically if you want to change redux state you should dispatch an action with parameters - action type and new data.
You can read more about redux dispatch here
I have made some research about possible ways to do it, but I can't find one that uses the same architecture like the one in the app I'm working on. For instance, React docs say that we should have a method which makes the HTTP request and then calls actions in different points (when request starts, when response is received, etc). But we have another approach. We use an action which makes the HTTP call and then dispatches the result. To be more precise, my use case is this:
// action to get resource A
getResourceA () {
return dispatch => {
const result = await axios.get('someLink');
dispatch({
type: GET_RES_A,
payload: result
});
};
}
// another action which needs data from resource A
getSomethingElseByIdFromA (aId) {
return async dispatch => {
const result = await axiosClient.get(`someLink/${aId}`);
dispatch({
type: GET_SOMETHING_BY_ID_FROM_A,
payload: result
});
};
}
As stated, the second action needs data from the first one.
Now, I know of two ways of doing this:
return the result from the first action
getResourceA () {
return async dispatch => {
const result = await axios.get('someLink');
dispatch({
type: GET_RES_A,
payload: result
});
return result;
};
}
// and then, when using it, inside a container
async foo () {
const {
// these two props are mapped to the getResourceA and
// getSomethingElseByIdFromA actions
dispatchGetResourceA,
dispatchGetSomethingElseByIdFromA
} = this.props;
const aRes = await dispatchGetResourceA();
// now aRes contains the resource from the server, but it has not
// passed through the redux store yet. It's raw data
dispatchGetSomethingElseByIdFromA(aRes.id);
}
However, the project I'm working on right now wants the data to go through the store first - in case it must be modified - and only after that, it can be used. This brought me to the 2nd way of doing things:
make an "aggregate" service and use the getState method to access the state after the action is completed.
aggregateAction () {
return await (dispatch, getState) => {
await dispatch(getResourceA());
const { aRes } = getState();
dispatch(getSomethingElseByIdFromA(aRes.id));
};
}
And afterward simply call this action in the container.
I am wondering if the second way is all right. I feel it's not nice to have things in the redux store just for the sake of accessing them throughout actions. If that's the case, what would be a better approach for this problem?
I think having/using an Epic from redux-observable would be the best fit for your use case. It would let the actions go throughout your reducers first (unlike the mentioned above approach) before handling them in the SAME logic. Also using a stream of actions will let you manipulate the data throughout its flow and you will not have to store things unnecessary. Reactive programming and the observable pattern itself has some great advantages when it comes to async operations, a better option then redux-thunk, sagas etc imo.
I would take a look at using custom midleware (https://redux.js.org/advanced/middleware). Using middleware can make this kind of thing easier to achieve.
Something like :
import {GET_RESOURCE_A, GET_RESOURCE_B, GET_RESOURCE_A_SUCCESS, GET_RESOURCE_A_ERROR } from '../actions/actionTypes'
const actionTypes = [GET_RESOURCE_A, GET_RESOURCE_B, GET_RESOURCE_A_SUCCESS, GET_RESOURCE_A_ERROR ]
export default ({dispatch, getState}) => {
return next => action => {
if (!action.type || !actionTypes.includes(action.type)) {
return next(action)
}
if(action.type === GET_RESOURCE_A){
try{
// here you can getState() to look at current state object
// dispatch multiple actions like GET_RESOURCE_B and/ or
// GET_RESOURCE_A_SUCCESS
// make other api calls etc....
// you don't have to keep stuff in global state you don't
//want to you could have a varaiable here to do it
}
catch (e){
} dispatch({type:GET_RESOURCE_A_ERROR , payload: 'error'})
}
}
}
My react app isn't re-rendering when updating my data. I think its related to updating the state with an array, but I can't quite figure out exactly what is causing the issue.
Here is my action
export const fetchChannels = uid => dispatch => {
db.ref(`chat/userchats/${uid}`).on("value", snapshot => {
if (snapshot.exists()) {
let userchats = snapshot.val()
let channels = []
Object.values(userchats).map(function(chat, i) {
db.ref(`chat/chats/${chat}`).on("value", snap => {
let chatData = snap.val()
channels.push(chatData)
})
})
dispatch({
type: "FETCH_CHANNELS",
payload: channels
})
}
})
}
And my reducer
case 'FETCH_CHANNELS':
return {
...state,
channels:action.payload
}
Since channels is set to an empty array, I'm guessing that redux is not recognizing this as a change of state? But can't quite get the right fix.
Edit
I console.logged snapshot.exists() prior to the if statement, and that came back as True.
I also logged channels prior to the dispatch and this seems to be where there is an issue. channels does not return the updated values, but rather the previous values when updated.
Here is the code I am using to add the channel item to userchats. The action I am looking for is that a user creates a channel, which creates a channel with the member uid's and chat information. That information is then pushed to the userchats directory and is pushed in to those users channels.
export const createChannel = (name,members,purpose) => dispatch =>{
db.ref(`chat/chats/`).push({name,members,purpose})
.then(result=>{
let key = result.getKey()
members.map(function(member,i){
db.ref(`chat/userchats/${member}`).push(key)
.then(result=>{})
.catch(error=> {
console.log(error)
})
})
})
.catch(error => {
console.log(error);
})
}
When I console.log at the end of the createChannel(), and console.log prior to the channels dispatch, in fetchChannel(). When I create the channel, it shows that fetchChannels updates first, and createChannel() second, even though createChannel() is called first, and fetchChannel() is simply listening for an update.
Here is the element I am attempting to rerender when props is updated
{Object.keys(this.props.channels).map(function(item,i){
return(
<Channel channel={this.props.channels[item].name} key={i} notification={''} selected={this.state.selected} item={item} onClick={()=>this.selectChannel(item)}/>
)
},this)}
Thanks for the responses Josh.
I was able to solve my issue. I had to switch the order of database entries. Because the chats push was triggering the userchats update before it was able to create the channels entry. Creating a key and using that to create userchats entries first solved the update issue.
export const createChannel = (name,members,purpose) => dispatch =>{
let newKey = db.ref().push().getKey()
members.map(function(member,i){
db.ref(`chat/userchats/${member}`).push(newKey)
.then(result=>{})
.catch(error=> {
console.log(error)
})
})
db.ref(`chat/chats/${newKey}`).set({name,members,purpose})
}
I'm trying to make an app using API from https://swapi.co/
They have no pictures in their response so I thought to add them myself after fetching the data.
I'm fetching data from API in action like that
export const fetchCharacters = () => async dispatch => {
const response = await fetch('https://swapi.co/api/people/');
const data = await response.json();
const results = await data.results;
dispatch({
type: FETCH_CHARACTERS,
payload: results
});
}
After that in reduce
case FETCH_CHARACTERS :
return {
...state,
characters: action.payload
}
To load this data to my component:
componentDidMount() {
this.props.fetchCharacters();
}
After this this.props.characters is an array of objects.
So I've been thinking how can I add pictures for each character?
I'm thinking something like mapping over objects, adding new property to each one. And to assign right picture to character I'm thinking naming pictures 0.jpg, 1.jpg, and then in map function just doing somehing like:
character.pic = `link/${index}.jpg`;
Is it a good way of achieving this? Or is there a better way?
you can not change props value. i am sharing 2 way that you can use.
One is
const resultObj = {};
resultObj.result = await data.results;
resultObj.imgPath = "path of image"
dispatch({
type: FETCH_CHARACTERS,
payload: resultObj
});
Second is here
this.setState({characters:this.props.fetchCharacters})
Now you can add key in state