What is the correct way to call updateCachedData on a click event in a component that uses the RTKQ query? - reactjs

I can only think of storing a reference to updateCachedData somewhere globally and use it in that click event but I am not sure this is the React way of doing this.
I have a notifications feed built with a Socket.IO server.
By clicking on a notification it should get deleted from the list. (The list should show only unread notifications.)
But when deleting from the list I create a new array as state in the notifications pane.
When I receive a new notification, all the deleted notifications return back - this is not what I intended.
How can I change the cache entry, more precisely remove items from it without remaking the request for all the notifications?
There are no error messages.
Code
getNotifications: build.query<
IDbNotification[],
IGetNotificationsQueryParams
>({
query: (params: IGetNotificationsQueryParams) => ({
url: `notifications?authToken=${params.authToken || ""}&limit=${
params.limit
}&userId=${params.userId || ""}${
params.justUnread ? "&justUnread" : ""
}`,
method: "GET"
}),
keepUnusedDataFor: 0,
async onCacheEntryAdded(
arg,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
) {
const { myIo, connectHandler } = getWebSocketConnection(
"notifications",
clone({
subscribtions: arg.userId
? getFollowedUserIds().concat({
uid: arg.userId,
justUnread: arg.justUnread
})
: getFollowedUserIds()
})
);
const listener = (eventData: IDbNotification) => {
if (
(eventData as any).subscriber === arg.userId &&
(!arg.justUnread || typeof eventData.readDateTime === "undefined")
) {
updateCachedData(draft => {
draft.unshift(eventData);
if (draft.length > arg.limit) {
draft.pop();
}
});
}
};
try {
await cacheDataLoaded;
myIo.on("notifications", listener);
} catch {}
await cacheEntryRemoved;
myIo.off("notifications", listener);
myIo.off("connect", connectHandler);
}
})

You can use updateQueryData - updateCachedData is just a shortcut for the current cache entry for convenience.
dispatch(
api.util.updateQueryData('getNotifications', arg, (draft) => {
// change it here
})
)
See this for more context: https://redux-toolkit.js.org/rtk-query/usage/optimistic-updates

Related

uswSWRInfinite shows stale data when mutating with local data

I use uswSWRInfinite to paginate data by a cursor (the id of the last loaded item). When I edit or delete items on page 2+ (for some reason, this doesn't happen on page 1), and I mutate by passing modified data, after revalidation, I see the old data again.
This does not happen if I either 1. mutate without local data (only revalidation) or 2. disable revalidation (only mutate with local data). Both together cause the bug.
Here's the relevant code:
useSWRInfinite setup:
const {
data: repliesPages,
size: repliesPagesSize,
setSize: setRepliesPagesSize,
isLoading: repliesLoading,
error: repliesLoadingError,
mutate: mutateRepliesPages,
} = useSWRInfinite(
getPageKey,
([commentId, lastReplyId]) => BlogApi.getRepliesForComment(commentId, lastReplyId));
The update/delete callbacks:
replies?.map(reply => (
<CommentBody
comment={reply}
onReplyCreated={addLocalReply}
key={reply._id}
onCommentUpdated={(updatedReply) => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.map(existingReply => existingReply._id === updatedReply._id ? updatedReply : existingReply);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
onCommentDeleted={() => {
const updatedRepliesPages = repliesPages?.map(page => {
const updatedReplies = page.comments.filter(existingReply => existingReply._id !== reply._id);
const updatedPage: GetCommentsResponse = { ...page, comments: updatedReplies };
return updatedPage;
});
mutateRepliesPages(updatedRepliesPages); // this works properly if I don't pass data or set revalidate : false
}}
/>
));
The callbacks are triggered after we got the updated item back from the server:
async function onSubmit({ text }: { text: string }) {
if (!text) return;
try {
const updatedComment = await BlogApi.updateComment(comment._id, text);
onCommentUpdated(updatedComment);
} catch (error) {
console.error(error);
alert(error);
}
}
async function deleteComment() {
try {
setDeleteInProgress(true);
await BlogApi.deleteComment(comment._id);
onCommentDeleted();
} catch (error) {
console.error(error);
alert(error);
} finally {
setDeleteInProgress(false);
}
}
Here's a recording of the problem happening:
The behavior I expect, is that SWR shows the updated data after revalidation.
You need to fire a request to update the data on the server (BlogApi).
mutate() will update the data on the client side, but not on the server. You're updating the data locally, then the data revalidates (refetches), which replaces the local data with the server data, undoing your updates.
Add the appropriate request (probably POST) to your code. You can add it immediately before mutateRepliesPage(), or you can include it as part of the function passed to mutate's second argument like in this example.

RTK query use infinity scroll and pagination at the same time

Describtion:
I am trying to use infinity scroll and pagination at the same time.
I have a component which loads the user orders with infinity scroll and another component in different page which loads user order by pagination.
after i read the documentation I noticed we could use serializeQueryArgs, merge, forceRefetch to create these effects so I did it in my api slice.
Problem:
I have nice infinity scroll but my pagination works just like my infinity scroll
every time I click on a page the new data merged with my prev page.
so I decided to use extera prams for my query just only for serializeQueryArgs mehtod and check if I call this api from the component with infinity scroll or pagination and it works better and now i have another problem for example when I'm in the page 1 and go to page 2 everything seems normal but when i back to page 1 i have duplicated the page one array.
here is my api Slice:
export const orderApi = ApiSlice.injectEndpoints({
tagTypes: ['Orders'],
endpoints: (builder) => ({
getOrdersCount: builder.query({
query: ({ page, size, other }) => ({
url: `/order/orders/count?${page ? `page=${page}` : ''}${
size ? `&size=${size}` : ''
}${other || ''}`,
method: 'GET'
}),
transformResponse: (res) => res // just only one number
}),
getOrderList: builder.query({
providesTags: (result) => {
return result.map((item) => ({ type: 'Orders', id: item.id }));
},
async queryFn({ page, size, other }) {
const portfolioList = await axios.get(
`/order/orders?${page ? `page=${page}` : ''}${size ? `&size=${size}` : ''}${
other || ''
}`
);
if (portfolioList.error) return { error: portfolioList.error };
const endpoints = portfolioList.data.map((item) =>
axios.get(`/market/instruments/${item.insRefId}`)
);
let error = null;
let data = null;
try {
data = await Promise.all(endpoints).then((res) => {
return res.map((item, index) => {
return { ...item.data, ...portfolioList.data[index] };
});
});
} catch (err) {
error = err;
}
return data ? { data } : { error };
},
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointName, queryArgs }) => {
const { infinity, page, size } = queryArgs;
if (infinity) return endpointName;
return endpointName + page + size;
},
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
currentCache.push(...newItems);
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
console.log('currentArg', currentArg);
return currentArg !== previousArg;
},
})
})
});
any solution or best practice ?

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

React native: how do I wait for a state to be set, before I call another state related operation?

I am writing a chat app. Users can search for other users, and then press the "Message" button. Then I navigate to ChatScreen.js. If both users have been messaging each other, I set the chatId variable accordingly. If they have not messaged each other before I dont create chatId, until the ery first message has been sent. When the first message is sent, I first, create new chat, store its properties (user ids, chatId, etc) in my db and then I sent the first message. The problem is that I store chatId as a state variable, and when I create the chat I call setChatId(id). setChatId() is not synchronous call, so by the time when I need to send message with sendText(text, chatId); my chatId is undefined even though I have already created a chat and I have called setChatId.
How can I avoid this error? Ofc, I can check if chatId == undefined then calling sendText(text, id), otherwise calling sendText(text, chatId). Is there a better/neath way to avoid the undefined check?
Here is part of my code:
...
import {
createChat,
} from "./actions";
...
function ChatScreen(props) {
...
const [chatId, setChatId] = useState(props.route.params.chatId);
...
const setupChat = async () => {
try {
await createChat(user.id, setChatId);
props.fetchUserChats();
} catch (error) {
console.error("Error creating chat: ", error);
}
};
async function handleSend(messages) {
if (!chatId) {
// creating chat
await setupChat();
}
const text = messages[0].text ? messages[0].text : null;
const imageUrl = messages[0].image ? messages[0].image : null;
const videoUrl = messages[0].video ? messages[0].video : null;
const location = messages[0].location ? messages[0].location : null;
//assuming chatId is already setup but it is not
if (imageUrl) {
sendImage(imageUrl, chatId, setSendImageError);
} else if (location) {
sendLocation(location, chatId, setLocationError);
} else if (videoUrl) {
sendVideo(videoUrl, chatId, setSendImageError);
} else {
sendText(text, chatId);
}
}
...
}
My createChat function from actions.js file
export async function createChat(otherUid, setChatId) {
let chatId = firebase.auth().currentUser.uid + "_" + otherUid;
await firebase
.firestore()
.collection("Chats")
.doc(chatId)
.set({
users: [firebase.auth().currentUser.uid, otherUid],
lastMessage: "Send the first message",
lastMessageTimestamp: firebase.firestore.FieldValue.serverTimestamp(),
})
.then(() => {
console.log("doc ref for creatign new chat: ", chatId);
setChatId(chatId);
})
.catch((error) => {
console.error("Error creating chat: ", error);
});
}
Instead of using a state variable, I would advise you to use useRef(). This would be a good solution to your problem.Eg Define it this way
const chatId = useRef(null),
then set it this way chatId.current = yourChatId
and get it this way chatId.current. I hope this solves your problem

How to call an API even adBlocker is enabled in React?

I am using detectAdBlock library to detect ad-blocks.
Based on the response,I am calling a checkin API.
That API gives a boolean if user visits the page for the first time, it returns true.
When the response is true, I have created a profile popup modal which only appears when the Check-in API response is true.Currently I have a logic like if ad-blocker is enabled, I am not calling that API.When the user disables the ad-block, then only the API was getting called.
Now I have added a close button on my Ad-Block popup and because of that the user can turn off the ad-block popup but are not able to see the profile Popup because the check-In API doesn't get called because the ad-blocker library response is still true.
Here is the code -->
Main.js
componentDidMount() {
detectAnyAdblocker().then((detected) => {
if (!detected) {
this.props.checkInAPI(params).then((resp) => {
localStorage.setItem('firstTimeUserVisit', resp && resp.data === true);
const userId = localStorage.getItem('userId');
});
}
});
}
render() {
return (
<AdBlock />
)
}
Profile.js (Popup condition)
const firstTimeCheckIn = localStorage.getItem('firstTimeUserVisit');
if (firstTimeCheckIn === 'true' && !adBlock) {
setShowProfilePopup(true);
}
return (
{showProfilePopup && (<PopupModal> ..... </PopupModal>)}
)
AdBlock.js
let closeAdBlock = false;
const AdBlock = ({ eventData }) => {
const [adBlock, setAdBlock] = useState(false);
const onCloseAdBlock = () => {
setAdBlock(false);
closeAdBlock = true;
};
useEffect(() => {
if (!closeAdBlock) {
detectAnyAdblocker().then((detected) => {
if (detected) {
setAdBlock(true);
}
});
}
}, [adBlock]);
return (
<PopupModal
id="adblock-popup"
custClassName={'adblock-popups'}
onCloseFunc={() => onCloseAdBlock()}
showModal={adBlock}
>.....</PopupModal>
)
I want the API should be called whether a user has ad-block enabled or disabled but the profile popup should also be shown if user visits the page for the first time

Resources