ReactQuery does not always mark data as changed when refetching - javascript

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.

Related

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/

How to make useEffect rerender after the data is changed?

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.

How to implement this without triggering an infinite loop with useEffect

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]);

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.

[TypeError]: Firebase orderByChild not working

I'm trying to query my database such that it retrieves an ordered list based on each child key. I do it as follows (see below), but "TypeError" happens. That is ordered at random when using .on('value', snapshot =>. I can't fix that, do you guys have any ideas to realize?
The Error
TypeError: In this environment the sources for assign MUST be an object. This error is a performance optimization and not spec compliant.
Realtime Database Query
Source Code
export const messagesFetch = (room) => {
return (dispatch) => {
firebase.database().ref(`/rooms/${room.roomId}/messages`)
.once('child_added', snapshot => {
dispatch({ type: 'messages_fetch_success', payload: snapshot.val() });
})
};
};
child_added will create a new snapshot for each message, instead of returning a list of messages in one object.
You might want to look at .once('value') instead of once('child_added').
As you wanted an ordered list I added the query orderByKey() which will return the messages in key order.
export const messagesFetch = (room) => {
return (dispatch) => {
firebase.database().ref(`/rooms/${room.roomId}/messages`)
.orderByKey()
.once('value')
.then(snapshots => {
snapshots.forEach(snapshot => {
dispatch({ type: 'messages_fetch_success', payload: snapshot.val() });
return false;
});
});
};
};
Is a react-native app, right?! If yes add a flag in question.
In firebase 'child_added' fires for each child and ever a new child has add, like sketchthat says. With 'once' you not listen the node, but, because 'child_added' you fires your function for every child. In other words your function return differents values (snapshot) for a same constant.

Categories