How to implement this without triggering an infinite loop with useEffect - javascript

So I have a situation where I have this component that shows a user list. First time the component loads it gives a list of all users with some data. After this based on some interaction with the component I get an updated list of users with some extra attributes. The thing is that all subsequent responses only bring back the users that have these extra attributes. So what I need is to save an initial state of users that has a list of all users and on any subsequent changes keep updating/adding to this state without having to replace the whole state with the new one because I don't want to lose the list of users.
So far what I had done was that I set the state in Redux on that first render with a condition:
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
}
userList = users || usersFromProp
})
The above was working fine as it always saved the users sent the first time in the a prop and always gave priority to it. Now my problem is that I'm want to add attributes to the list of those users in the state but not matter what I do, my component keeps going into an infinite loop and crashing the app. I do know the reason this is happening but not sure how to solve it. Below is what I am trying to achieve that throws me into an infinite loop.
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
} else {
//Users already exist in state
const mergedUserData = userDataFromApi.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
})
setUsers(mergedUserData)
}
}, [users, setUsers, userDataFromApi])
So far I have tried to wrap the code in else block in a separate function of its own and then called it from within useEffect. I have also tried to extract all that logic into a separate function and wrapped with useCallback but still no luck. Just because of all those dependencies I have to add, it keeps going into an infinite loop. One important thing to mention is that I cannot skip any dependency for useCallback or useEffect as the linter shows warnings for that. I need to keep the logs clean.
Also that setUsers is a dispatch prop. I need to keep that main user list in the Redux store.
Can someone please guide me in the right direction.
Thank you!

Since this is based on an interaction could this not be handled by the the event caused by the interaction?
const reducer = (state, action) => {
switch (action.type) {
case "setUsers":
return {
users: action.payload
};
default:
return state;
}
};
const Example = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users)
useEffect(() => {
const asyncFunc = async () => {
const apiUsers = await getUsersFromApi();
dispatch({ type: "setUsers", payload: apiUsers });
};
// Load user data from the api and store in Redux.
// Only do this on component load.
asyncFunc();
}, [dispatch]);
const onClick = async () => {
// On interaction (this case a click) get updated users.
const userDataToMerge = await getUpdatedUserData();
// merge users and assign to the store.
if (!users) {
dispatch({ type: "setUsers", payload: userDataToMerge });
return;
}
const mergedUserData = users.map(existingUser => {
const matchedUser = action.payload.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
dispatch({ type: "setUsers", payload: mergedUserData });
}
return (
<div onClick={onClick}>
This is a placeholder
</div>
);
}
OLD ANSWER (useState)
setUsers can also take a callback function which is provided the current state value as it's first parameter: setUsers(currentValue => newValue);
You should be able to use this to avoid putting users in the dependency array of your useEffect.
Example:
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);

Related

React Native Firebase adding and getting data

wondering if anyone can assist me in this matter. I'm following the documentation for https://rnfirebase.io/firestore/usage. it does not work for my use case for some reason.
I just need to set the data, which it works and then read it back so i can push it onto my state and i'll render it.
I just can't read the data back properly. This addItemFunction is trigger when when user click on a button to add.
const addItemFunction = async (numb,exercise) =>{
firestore().collection(userEmail).get().then((snap) =>{
if(!snap.empty){
var finalID = uuid.v4();
firestore().collection(userEmail).doc(final).update({
[finalID]:{
exe:[exercise],
num:[numb],
}
}).then(() =>{
//RETURN SNAPSHOT NOT WORKING
console.log('user_added');
firestore().collection(userEmail).doc(final).onSnapshot(documentSnapshot =>{
console.log("here" + documentSnapshot.data());
});
}
Thanks for your time.
If you are using react with hooks I would suggest you put the onSnapshot listener in a useEffect hook:
useEffect(() => {
const unsubscribe = firestore
.collection(collectionName)
.doc(docId)
.onSnapshot(
(documentSnapshot) => {
const document = documentSnapshot.data();
console.log(document)
},
(error: Error) => {
throw error;
}
);
return () => unsubscribe();
}, [ docId, collectionName]);
this approach will separate concerns and the snapshots will run every time there is a change on the document, then where I put the console.log you could set the document to state.
Another approach will be to use get() instead of onSnapshot like:
const addItemFunction = async (numb,exercise) =>{
firestore().collection(userEmail).get().then((snap) =>{
if(!snap.empty){
var finalID = uuid.v4();
firestore().collection(userEmail).doc(final).update({
[finalID]:{
exe:[exercise],
num:[numb],
}
}).then(() =>{
console.log('user_added');
firestore().collection(userEmail).doc(final).get().then(() => {
console.log("here" + documentSnapshot.data());
})
}
}
}
this approach will not subscribe to changes and it will return the new updated document every time you call the addItemFunction

I created a react native app and have to refresh my screen every time in order to get the newly added data from firebase. I am using hooks

I created a react native app and have to refresh my screen every time in order to get the newly added data from firebase. I'm new to firebase and I thought I can use snapshot to get the current data but I still have to refresh my app every time a new event is created in order to see all the updated events on this view. Any help would be appreciated
export default function EventsHostedScreen() {
const navigation = useNavigation();
const [eventsData, setEventsData] = useState([]);
useEffect(() => {
async function fetchData() {
const currentUser = await firebase.auth().currentUser.uid;
result = [];
const eventsCollection = firebase.firestore().collection('events');
eventsCollection.get().then((snapshot) => {
snapshot.docs.forEach((doc) => {
if (doc.exists === true && doc.data().userId !== null) {
if (doc.data().userId === currentUser) {
result.push(doc.data());
}
}
});
setEventsData(result);
});
console.log('RESULT==>', result);
}
fetchData();
}, []);
You can listen to changes to a document or collection with the onSnapshot method. In addition to that, I would suggest a couple of changes to your code.
It seems to me like you want to query for documents where the userId is same as the current user's id. It would be easier to include this in the query with the where method. That way you won't have to filter the documents with if statements like you currently are. You will also save on Firestore reads, as right now you are getting all events, but with the where method you will only read the documents where the equality clause is true.
I would also include a check for whether you have the currentUser available, unless you are 100% sure this component won't ever be rendered while the currentUser is loading. And you don't need to await the currentUser and therefore don't need an async function anymore.
With these changes your useEffect could look something like the following.
useEffect(() => {
// Check if currentUser exists to avoid errors
if (!firebase.auth().currentUser) {
return;
}
const currentUser = firebase.auth().currentUser.uid;
// Create subscription to listen for changes
const unsubscribe = firebase
.firestore()
.collection('events')
.where('userId', '==', currentUser)
.onSnapshot((snapshot) => {
const result = [];
snapshot.forEach((doc) => {
result.push(doc.data());
});
setEventsData(result);
});
// Remove the listener when component unmounts
return () => unsubscribe();
// Add currentUser to useEffect dependency array, so useEffect runs when it changes
}, [firebase.auth().currentUser]);

Component unable to fetch data from Firebase when navigating to it for the first time or coming back

Background
I'm building an app which displays a number of stores in the home screen. They are shown in a carousel which is filled up with information from a Firestore Collection and Firebase Storage. The user can navigate into each store by pressing on them. The Home Screen display works just fine every single time, but when navigating to one store components come back as undefined. This is the way I'm fetching the data:
export default function StoreDetailMain ({route}) {
const { storeId } = route.params
const [store, setStore] = useState()
useEffect(() => {
const fetchQuery = async () => {
const storeData = await firebase.firestore()
.collection('stores/')
.doc(storeId)
.get()
.then(documentSnapshot => {
console.log('Store exists: ', documentSnapshot.exists);
if (documentSnapshot.exists) {
console.log('Store data: ', documentSnapshot.data());
setStore(documentSnapshot.data())
console.log(documentSnapshot.data())
}
});
}
fetchQuery()
}, [storeId])
Then I'm rendering the information within tags as in <Text>{store.value}</Text>.
Problem
Navigating once to the store will always return a Component Exception: undefined is not an object (evaluating 'store.value'). However if I cut the "{store.value}" tags it works just fine. Then I can manually type them in again and they render perfectly. Once I go back to the Home Screen and try to go into another store I have to do it all again. Delete the calls for information within the return(), save the code, reload the app and type them in again.
What I have tried
Sometimes, not always, Expo will give me a warning about not being able to perform a React state update on an unmounted component. I thought this might be the problem so I gave it a go by altering my useEffect method:
export default function StoreDetailMain ({route}) {
const { storeId } = route.params
const [store, setStore] = useState()
useEffect(() => {
let mounted = true;
if(mounted){
const fetchQuery = async () => {
const storeData = await firebase.firestore()
.collection('stores/')
.doc(storeId)
.get()
.then(documentSnapshot => {
console.log('Store exists: ', documentSnapshot.exists);
if (documentSnapshot.exists) {
console.log('Store data: ', documentSnapshot.data());
setBar(documentSnapshot.data())
console.log(documentSnapshot.data())
}
});
}
fetchQuery()
}
return () => mounted = false;
}, [storeId])
This would not solve the issue nor provide any variation.
Question
Is this due to the unmounting/mounting of components? If so, wouldn't the useEffect method take care of it? If anyone could provide an explanation/solution it would be very much appreciated.
Thanks in advance.
Edit 1:
When the application fails to render the information, it doesn't print into the console the document snapshot. When it can render the data, it does log it. Thus the change in title.
try giving it a initial value
const [ store, setStore ] = useState({value: ''})
or render it conditionally
{ store?.value && <Text>{store.value}</Text> }
secondly, route.params is defined? When you switching screens, did u make sure u pass the params? Switching from stack navigator to tab navigator for example, may drop the params.

How to avoid warning: Cannot setState on unmounted component? REACT-NATIVE

im builiding a notebloc, so each time I go to the Edit note screen then go back, this warnings appears, heres the code:
when the app runs, the first component that rendes is Notes:
class Notes extends Component {
state = {
onpress: false,
array_notes: [],
selected_notes: [],
update: false,
}
note = "";
note_index = "";
note_number = "";
item_selected = 0;
onpress = false;
componentDidMount() {
const {navigation} = this.props;
this._unsubscribe = navigation.addListener("didFocus", () => this.fetch_notes());
}
componentWillUnmount() {
this._unsubscribe.remove();
}
fetch_notes = async() => { //fetch all the data
const data = await fetch_notes();
if (typeof data != "function") {
this.setState({array_notes: data.array_notes});
}else {
data();
}
}
as you can see, in the ComponentDidmount() I run the function fetch_data to set the data, heres the fetch_data function:
import AsyncStorage from "#react-native-community/async-storage";
export const fetch_notes = async () => {
try {
const data = JSON.parse(await AsyncStorage.getItem("data"));
if (data != null) {
return data;
}else {
return () => alert("with no data");
}
}catch (error) {
alert(error);
}
}
here everything works, because its only fetch the data, now when I go to edit note screen and I edit one of the notes, I need to save it, so when I go back I need to fetch the data again so it will update:
save_changes = async() => {
try {
const data = JSON.parse(await AsyncStorage.getItem("data"));
const index_to_find = this.array_notes.findIndex(obj => obj.note_number === this.note[0].note_number);
const edited_note = this.note.map((note) => {
note.content = this.state.content;
return {...note}
});
this.array_notes.splice(index_to_find, 1, edited_note[0]);
data.array_notes = this.array_notes;
await AsyncStorage.setItem("data", JSON.stringify(data));
}catch(error) {
alert(error);
}
when I get back to Notes screen the function runs and works, the data are updated, but the warning still appears, once I saved the edit note and go back, how can I avoid this?
This warning is thrown when you try to set the state of a component after it has unmounted.
Now, some things to point out here
The navigation flow, from what have you mentioned is like Notes --> Edit Notes --> Notes. Assuming you are using the StackNavigator from react-navigation, the Notes screen will not unmount when you navigate to Edit Notes screen.
The only screen unmounting is Edit Notes when you go back. So you should check the code to verify that you don't have any asynchoronous setState calls in that screen.
P.S : The syntax to remove the focus event listener is to just call the returned function as mentioned here.
So in your Notes screen inside componentWillUnmount it should be
componentWillUnmount() {
this._unsubscribe();
}
you need to use componentWillUnmount() function inside the function which you are unmounting.
You can use conditional rendering for mounting or unmounting any componets.

Functions in epic are firing out of order and I can't understand why

*SOLVED
The problem was in how I was creating and responding to the observable created by the firebase callback.
I also had way too much stuff going on inside my firebase callbacks.
I ended up splitting it up a bit more, using the firebase promise structure: https://firebase.googleblog.com/2016/01/keeping-our-promises-and-callbacks_76.html
and creating an Observable.fromPromise for the firebase callback within what is now called firebaseAPI.checkForUser.
*
Working epic:
export const contactFormFirebaseSubmitEpic = (action$) =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
const formattedEmail = firebaseAPI.getFormattedEmail(values);
const contactsRef = firebaseAPI.getContactsRef(formattedEmail);
return firebaseAPI.checkForUser(values, formattedEmail, contactsRef);
})
.flatMap((data) => concat(
of(firebaseAPI.recordUserAndUpdateDetails(data))
))
.flatMap((data) => concat(
of(firebaseAPI.setQuoteData(data))
))
.switchMap((x) => merge(
of(stopLoading()),
of(contactFormFirebaseSuccess())
));
// original question
Ok so, what I'm trying to achieve is to perform the first action (firebaseAPI.checkUserAndUpdate), then the next, then when both of them are done essentially discard what's there and send out two actions (contactFormFirebaseSuccess and stopLoading).
This all works fine except for one weird thing, the setQuoteData function always runs before the checkUser function. Does anyone know why this might be?
Also if there's a better way to lay this out I'd be very open to suggestions! Cheers. Also I've taken out quite a few variables and things that would make it even more complicated. Basically I just wanted to show that in each case I'm returning an observable from 'doing something with firebase'. But I don't think that's the problem as I have console logs in each of the firebase functions and the setQuoteData one just fires first and then executes the firebase stuff then when it's done the checkUserAndUpdate one runs.
export const contactFormFirebaseSubmitEpic = action$ =>
action$.ofType(START_CONTACT_FORM_FIREBASE_SUBMIT)
.flatMap((firebaseSubmitAction) => {
const values = firebaseSubmitAction.values;
return merge(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
.takeLast(1)
.mergeMap((x) => {
return merge(
of(contactFormFirebaseSuccess()),
of(stopLoading()),
);
});
});
const firebaseAPI = {
checkUserAndUpdate: (values) => {
const checkUserAndUpdateDetails = firebaseRef.once('value', snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(databaseValue, values);
});
return Observable.from(checkUserAndUpdateDetails);
},
setQuoteData: (value) => {
const setQuote = setQuoteData(values);
return Observable.from(setQuote);
},
};
const stopLoading = () => ({ type: STOP_BUTTON_LOADING });
const contactFormFirebaseSuccess = () => ({ type: SUCCESS });
checkUserAndUpdate: (values, contactsRef) => {
const checkUser$ = Observable.from(contactsRef.once('value').then(
snapshot => {
const databaseValue = snapshot.val();
checkUserExistsAndUpdateDetails(
values,
databaseValue,
contactsRef,);
})
);
return checkUser$;
},
const checkUserExistsAndUpdateDetails = (
values,
databaseValue,
contactsRef,
) => {
if (databaseValue) { console.log('user exists'); }
else {
console.log('new user, writing to database');
contactsRef.set({
name: values.name,
email: values.email,
phone: values.phone,
});
}
};
The problem is that merge does not maintain the order of the streams that you subscribe to, it simply emits events from any of the source streams regardless of what order they emit.
If you need to maintain order you should use concat instead of merge
i.e.
const values = firebaseSubmitAction.values;
return concat(
firebaseAPI.checkUserAndUpdate(values),
firebaseAPI.setQuoteData(values),
)
Side note, I don't know why you are using the of operator there, you already have Observables returned from your API so you can just pass those to merge or concat in this case.

Categories