Firebase Redux update not triggering render - reactjs

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})
}

Related

Can't access state in functional component in Reacht Native

I'm currently trying to build an app in React Native. Unfortunately, I'm having a hard time understanding the state management in functional components.
The fetch resolves successfully and gives me an array of activities and I store them in the component's state. But after that I want to make more fetches regarding these activities and for that i need to know each activities' ID, so I have to access the state. This doesn't work though, as there's only an empty array printed to the console from the last log.
From the printed timestamps I can see that everything executes in the desired order and I can easily access the state in other places and get the full array of activities, but why isn't it working here?
Here's the code:
const [activities, setActivities] = useState([]);
async function getActivites(cred){
const zeroLevel = Date.now();
fetch(`https://www.strava.com/api/v3/athlete/activities?access_token=${cred.access_token}`)
.then((res) => res.json())
.then((data) => {
for (const element of data) {
setActivities(oldActivities => [... oldActivities, element])
console.log(Date.now() - zeroLevel)
}
console.log('for-loop finished', Date.now() - zeroLevel)
})
.then(() => console.log(Date.now() - zeroLevel))
.then(() => console.log(activities))
}
I already tried to store the array in another object to make it more easily accessible, but I'm almost certain there's an easier way.
If data is an array, you don't need to iterate over it, you can just set the activites with data, instead of looping over it:
.then((data) => {
setActivities(data)
console.log('fetch finished', Date.now() - zeroLevel)
return data
})
.then((data) => {
data.map(activity => // do the fetch on each activity)
}
Or if you want to base the chained fetch on the state, then you can manually observe the change like this:
.then((data) => {
setActivities(data)
console.log('fetch finished', Date.now() - zeroLevel)
})
useEffect(() => {
activities.map(activity =>// do the fetch on each activity)
},[activities])

Unable to access properties of object stored in a React state

I am using React hooks and I need to store the data from my server which is an object, to a state.
This is my state:
const [sendingTime,setSendingTime] = useState({})
This is how I set it inside useEffect:
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
},[])
Now when I console.log the object state it returns an empty object. Although if I set the object to any variable and then console.log it it doesn't return an empty object. But both ways I am unable to access the properties of the object.
Edit
This is what I was doing previously:
const[time,setTime] = useState({
totalComplaintsTime: '00:00',
resolvedComplaintsTime: '00:00',
unresolvedComplaintsTime:'00:00',
assignedComplaintsTime:'00:00',
supervisorsTime:'00:00',
rejectedComplaintsTime:'00:00'
})
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setTime({
totalComplaintsTime: res.data.Complaints[0].updatedAt,
resolvedComplaintsTime: res.data.Resolved[0].updatedAt,
unresolvedComplaintsTime: res.data.Unresolved[0].updatedAt ,
assignedComplaintsTime: res.data.Assigned[0].updatedAt ,
rejectedComplaintsTime: res.data.Rejected[0].updatedAt,
supervisorsTime: res.data.Supervisors[0].updatedAt
})
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
// getTime()
getTime()
},[])
And this is how I used the states to set the dynamic values :
Last updated at {time.resolvedComplaintsTime}
This is working perfectly fine but I wanted to store the data in an object state and then access it, which would've been easier and more efficient. Also I wanted to pass this object to another component. That is why I wanted to make a state and store the data in that state.
Solved
So the main problem was accessing the data throughout the component. This is the solution:
sendingTime is being initialized but only when a render occurs. So we add a piece of code to check if that state is initialized or not.
This is where I wanted to display the data.
<div key={sendingTime.length} className={classes.stats}>
<UpdateIcon fontSize={"small"} /> Last updated at{" "}
{Object.keys(sendingTime).length > 0 &&
sendingTime.Complaints[0].updatedAt}
</div>
This way I can access the properties of the object stored in the sendingTime state very easily.
setSendingTime comes from a useState so it is asynchronous:
When you call:
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
The state sendTime has not been updated, so it display the init value which is {}
The other answers are correct, setState is asynchronous so you will only be able to get sendingTime's new value on the next re-render. And as #Enchew mentions, you probably don't want to set an object as that value most likely.
Try this instead:
const [data, setData] = useState(undefined)
const getTime = () => {
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(({ data }) => {
console.log(data)
setData(data)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
}, [])
if (!data) return <p>No data...</p>
return (
<>
<p>Complaints: {data.Complaints[0].updatedAt}<p>
<p>Resolved: {data.Resolved[0].updatedAt}<p>
<p>{/* ...etc... */}</p>
</>
)
You should see the value you're expecting to see in the console.log because you're using the locally scoped variable, not the asynchronous value, which will only be updated on the next re-render.
setSendingTime works asynchronously and your object may have not been saved in the state yet. Also pay attention to the way you save the time in your state, you are wrapping the result of the data in another object with property sendingTime, which will result in the following object: sendingTime = { sendingTime: {/*data here */} }. If you are running for sendingTime: {/*data here */} try the following:
const getTime = () => {
axios
.get('https://localhost:1999/api/getLastUpdatedTime')
.then((res) => {
console.log(res.data);
setSendingTime(res.data);
console.log('sendingTime is coimng', sendingTime);
})
.catch((err) => console.error(err));
};
Also have a look at this - how to use hooks with a callback: https://www.robinwieruch.de/react-usestate-callback

What is the best way to mount a component when you need a prop from another one

I have two actions. Sometimes the value returns undefined so instead of receiving the users post I receive all the post. I need to find a better way to get my functions to work together.
Right now I have:
componentDidMount(){
this.props.fetchUser();
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
}
Because it can't get the userId right away so it won't add the parameter to the end of my user search so it gives me all post.
If I try to do something else like:
componentDidMount(){
this.props.fetchUser();
}
componentWillReceiveProps(){
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
}
It will loop give me the post but loop infinitly in the console.
Also putting the code below in the render will also make it loop forever.
let userId = this.props.user.map((user) => (user.id));
console.log(userId)
this.props.fetchPostsFromUser(userId);
What is the best way to go about this so it works how I want it to.
EDIT
export const fetchUser = () => dispatch => {
Promise.all([fetch('http://10.6.254.22:5000/userinformation/3')])
.then(([res1]) => {
return Promise.all([res1.json()])
})
.then(([user]) => dispatch ({
// set state in here
type: FETCH_USER,
payload: user
// console.log(res1)
// this.setState({ results: res1, posts: res2 })
// this.setState({ posts: res2 })
}));
}
You want to fetch posts 1 time, but can't do it on mount because you don't have the userId yet. If you do it in componentWillReceiveProps you get an infinite loop -- because fetching posts updates props.
You need to run some code when the component updates, but only when you have a user id and didn't previously have a user id. componentWillReceiveProps is discouraged and deprecated so we will use componentDidUpdate which gets prevProps as it's first paramater.
Using componentDidUpdate:
componentDidUpdate(prevProps) {
let prevUserId = prevProps.user.map((user) => (user.id));
let userId = this.props.user.map((user) => (user.id));
if(!prevUserId && userId) {
this.props.fetchPostsFromUser(userId);
}
}
If you're curious how this might look using hooks, here's an example:
const userId = props.user.map((user) => (user.id))
React.useEffect(() => {
if(userId) {
props.fetchPostsFromUser(userId)
}
}, [userId])
The hook will only be called when userId has changed value, and will only fetch posts if there is a userId

Lookup multiple Firebase entries using Redux action

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!

Redux - Reducer not being called

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

Resources