How to make useEffect rerender after the data is changed? - javascript

I want the useEffect to fetch and render rooms data. And rerender the data when the new room is being added to rooms
Code to fetch data and display data with useEffect and an empty dependency.
const [rooms, setRooms] = useState([]);
const getRoomsData = async () => {
const querySnapshot = await getDocs(collection(db, "rooms"));
const data = querySnapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}));
setRooms(data);
};
useEffect(() => {
getRoomsData();
console.log("rerendered");
}, []);
With this code, after I added new room to rooms. I need to manually refresh the page to see the new data rendered. What should I do to make it rerender itself after new "room" is added to rooms?
Code to add room:
const roomName = prompt("please enter name for chat room");
if (roomName) {
addDoc(collection(db, "rooms"), {
name: roomName,
});
}
};
I tried adding rooms to the useEffect dependency, which has the result I want (no need to refresh), but it is only because it is rendering infinitely, which is bad. What is the proper way of doing it?

You are getting infinite re-renders because the function inside the useEffect is updating the state of rooms which is in the dependency array of the effect. This will always cause infinite re-renders.
To answer your question verbatim:
"How do I make it rerender only when the data is changed?"
Since rooms and data are set to be the same, you can keep your useEffect how it is, but then create another useEffect to fire only when the component mounts to call getRoomsData().
const [rooms, setRooms] = useState([]);
const getRoomsData = async () => {
const querySnapshot = await getDocs(collection(db, "rooms"));
const data = querySnapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}));
setRooms(data);
};
useEffect(() = > {
getRoomsData();
}, [])
useEffect(() => {
console.log("rerendered");
}, [rooms]);
I think the real crux of solving your issue is knowing when to call getRoomsData(), because depending on that, you will change the useEffect dependency array to fit that need.

I can think of two approaches to solving this problem, without having to use useEffect, the first one being a workaround where you update the rooms state locally without having to fetch it from the server, i.e, you are basically constructing and appending an object to the existing array of rooms, this is possible only if you know the structure of the object, looking at your getRoomsData function, it seems that you are returning a data array with each item having the following object structure.
{
id: <DOCUMENT_ID>,
data: {
name: <ROOM_NAME>
}
}
So, when you add a room, you can do something like this:
const roomName = prompt("please enter name for chat room");
if (roomName) {
addDoc(collection(db, "rooms"), {
name: roomName,
});
let newRoom = {
id: Math.random(), //random id
data: {
name: roomName
}
}
setRooms([...rooms,newRoom])
};
This way, you can also reduce the number of network calls, which is a performance bonus, but the only downside is that you've to be sure about the object structure so that the results are painted accordingly.
In case, you are not sure of the object structure, what you could do is instead invoke getRoomsData after you add your new room to the collection, like so:
const roomName = prompt("please enter name for chat room");
if (roomName) {
addDoc(collection(db, "rooms"), {
name: roomName,
}).then(res=>{
getRoomsData();
})
};
This is would ensure to fetch the latest data (incl. the recently added room) and render your component with all the results.
In either of the methods you follow, you could use your useEffect only once when the component mounts with an empty dependency array.
useEffect(() => {
getRoomsData();
//console.log("rerendered");
}, []);
By doing this, you first basically display the data when the user visits this page for the first time/when the components mount for the first time. On subsequent additions, you can either append the newly added room as an object to the rooms array after adding it to the database or add it to the database and fetch all the results again, so that the latest addition is rendered on the screen.

Related

ReactQuery does not always mark data as changed when refetching

I am currently trying to use react-query to fetch data for use in a react-table. This is what i currently have, i omitted the table stuff for simplicity:
const { data, refetch } = useQuery(['users'], api.user.getAll);
useEffect(() => {
console.log('data changed')
}, [data]);
// this triggers 'data changed'
const createUser = useMutation((user: IUser) => api.user.create(user), {
onSuccess: () => {
refetch();
console.log('refetched')
},
});
// this does not
const updateUser = useMutation((user: IUser) => api.user.update(user), {
onSuccess: () => {
refetch();
console.log('refetched')
},
});
const onCreateClick = () => {
const newUser: IUser = {
id: 0,
userName: 'test',
email: 'test#mail.de'
}
createUser.mutate(newUser);
};
const onEditClick = (user: IUser) => {
user.userName = 'New Name'
updateUser.mutate(user);
};
console.log(data)
// ... render to table
When adding (or removing) a user everything works as expected. However when i update the data of an existing user the useEffect hook that tracks if data changed does not trigger (and for the same reason the react-table does not show the updated values).
The data does get fetched as expected in both cases and the console.log at the end does log the array with the updated values. It almost seems like the data field returned by useQuery does not get marked as changed for arrays if its length doesn't change.
I don't understand this, since this is new data that got fetched from an api and thus should always get treated as changed.
I am using axios under the hood to do the fetching if that is relevant.
What am i doing wrong, any ideas?
I found the issue:
user.userName = 'New Name'
This was a reference to a user inside of data. Never edit the values in data returned by useQuery in place. By doing this the newly fetched data did match the existing one and thus useQuery did not mark it as changed.

React Apollo GraphQL what is the best way to fetch a partial data after CUD(Creating, Updating, Deleting)?

I get todo list with useQuery.
const { data, refetch } = useQuery(GET_TODOS);
After creating a todo, I get todo list with refetch like below.
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: () => refetch()
});
const handleAddTodo = useCallback((todoArgs) => {
addTodo({ variables: todoArgs });
}, []);
But It is obviously wasted time.
I tried to update only in an updated part. for that, I saved todos into a state and I changed this.
const [todos, setTodos] = useState([]);
...
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: (updatedData) => {
setTodos((prevTodos) => {
const newTodos = prevTodos.map((todo) => todo.id === updatedData.id ? updatedData : todo);
return newTodos;
});
}
}
...
useEffect(() => {
setTodos(data);
}, [data]);
...
But I'm not sure It is a right way. I think there may be an official way for updating a part of data.
What's the best way to fetch a partial data after Creating, Updating, Deleting?
I'm using 'no-cache' as a default option in the project.
Managing the query response in a new state seems a bit overkill to me.
In fact, Apollo GraphQL client automatically refetch the updated data, as long as you are returning the updated data id field in the mutation result.
For other cases, you may want to use a custom update function option.
You can read more about that here:
https://www.apollographql.com/blog/apollo-client/caching/when-to-use-refetch-queries/

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.

Firestore freaking out

I just opened my project under another domain (the production url) and when I opened the network requests I saw this:
https://i.imgur.com/NxgTmIf.mp4
This took forever (8 min or more.) and my CPU was hot like hell, What did I do wrong?
My app is quite simple, I suspect the root of this is this block of code:
const [items, setItems] = useState([]);
const publish = async () => {
const batch = firestore.batch();
items.forEach(({ id }, index) => {
batch.update(firestore.collection('v1').doc(id), { '#': index });
});
await batch.commit();
};
const onCompletion = querySnapshot => {
const arr = [];
querySnapshot.forEach(document => {
const { vid: { id: vid }, '#': index } = document.data();
const { id } = document;
arr.push({ id, vid, index });
});
setItems(arr);
};
useEffect(() => {
const unsubscribe = firestore
.collection('v1')
.orderBy('#')
.onSnapshot(onCompletion);
return () => {
unsubscribe();
};
}, []);
useEffect(() => { publish(); }, [items]);
const handleSortEnd = ({ oldIndex, newIndex }) => {
if (oldIndex === newIndex) {
return;
}
setItems(arrayMove(items, oldIndex, newIndex));
};
Basically what this does is load a list of videos from a playlist on a firestore's collection then after the user add a new video or move up/down save again.
Any clues?
Edit: after this madness the app does few requests and works as expected.
There's multiple trips to loading your state I think, which by principle should be updated. Why not sort your incoming snapshot as desired before committing it to state. Give a condition to only do the sorting when 'items' has expected values.
Another way is 'setItems' as is from firestore and do sorting of data during the rendering of the state instead of hitting state before rendering.
Moreover, setting a new set of data to your state every time you sort it is mutation. React will take them as new data from before rendering which is not efficient.
If I follow this correctly, you've got a useEffect() that subscribes to changes to your 'v1' firestore collection. When the the component first loads and upon subsequent changes in the collection's documents, the onSnapshot event is triggered calling onCompletion. onCompletion then extracts all the documents from the collection to a new array which is then assigned to your items state. Your other useEffect() statement, triggers when items changes, calling the publish function which performs a batch update of documents in the same 'v1' collection ...which would trigger the onSnapshot again calling onCompletion and the cycle would repeat non-stop.
It might make more sense to retrieve the collection's documents once when the component loads and initialize the items state with the results then write the changed documents back to the collection when items is updated. You shouldn't need to retrieve the documents again with a collection subscription because you've already got them in your items array.

Categories