React component uses old data from previous API call - reactjs

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

Related

How to refresh the value stored in context on an action?

So, i am fetching data from api and doing some manipulation on it and providing it to a component from context.
After I update the form i want to fetch new values and do the same thing again but its not being called automatically
const { isLoading: assayMatrixIsLoading, data: matrixData } = useQuery('Assay Matrix Data', fetchAssayMatrixes);
useEffect(() => {
// console.log('context mount');
}, []);
useEffect(() => {
if (!assayMatrixIsLoading) {
const unflattenedData = getDataForAutocomplete(unflattenObject(matrixData));
setDataForAutoComplete(unflattenedData);
}
}, [assayMatrixIsLoading]);
return dataForAutoComplete ? (
<AssayContext.Provider
value={{
dataForAutoComplete,
validation,
...
<AssayContextProvider>
<CreateAssayStepper />
</AssayContextProvider>
so how can i auto call it let say on route change or on submit like that?

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

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

use SWR with depending request data

I'm trying to use SWR to fetch list of users connected to the logged in user id provided by a custom hook.
I can't put useSWR inside either useCallback or useEffect or if (loggedInAdvisor) { ... }... Can't figure out how to do it.
export const fetchDetailedAdvisorPrognoses = (
body: DetailedAdvisorPrognosesRequest
): Promise<DetailedAdvisorPrognoses[]> | null => {
const accessToken = getFromPersistance(ACCESS_TOKEN)
if (!accessToken) {
return null
}
return fetch('https://x/api/v2/advisors/prognoses', {
method: 'POST',
headers: {
...generateDefaultHeaders(),
'Content-Type': 'application/json',
Authorization: getAuthorizationHeader(accessToken),
},
body: JSON.stringify(body), // body data type must match "Content-Type" header
}).then(res => res.json())
}
function Workload(): ReactElement | null {
const { loggedInAdvisor } = useAuthentication()
// yesterday
const fromDate = moment()
.subtract(1, 'day')
.format('YYYY-MM-DD')
// 14 days ahead
const toDate = moment()
.add(13, 'days')
.format('YYYY-MM-DD')
const { data, error } = useSWR<DetailedAdvisorPrognoses[] | null>('fetchWorkloadData', () =>
'detailed',
fetchDetailedAdvisorPrognoses({
advisorIds: [loggedInAdvisor.id], // <-- I want to pause the query until loggedInAdvisor is defined
fromDate,
toDate,
})
)
// todo: display errors better
if (error) {
return <span>Error: {error.message}</span>
}
if (!data) {
return <LoadingV2 isLoading={!data} />
}
if (data && data.length > 0) {
// advisors prognoses is first element in data array
const [first] = data
const days: WorkloadDay[] = Object.keys(first.daysMap).map(date => ({
date,
value: first.daysMap[date],
}))
return <WorkloadLayout before={first.before} days={days} />
}
return null
}
SWR supports Conditional Fetching, instead of using an if branching, you need to pass null as a key (that's the mental modal of React Hooks too):
const { data, error } = useSWR(
loggedInAdvisor ? 'fetchWorkloadData' : null,
() => {...}
)
Updated 2021/12/10:
You can also fetch some data, that based on the result of another request using SWR, too:
const { data: user } = useSWR('/api/user', fetcher)
const { data: avatar } = useSWR(user ? '/api/avatar?id=' + user.id : null, fetcher)
In this case, if user isn't ready the second request will not start since the key will be null. When the first request ends, the second one will start naturally. This is because a re-render will always happen when user changes from undefined to some data.
You can use this method to fetch as many dependent resources as you want, with the best possible parallelism.

Duplication problem in TodoList application

I am creating a todo-list, the following function handleChange gets the id of the a todo component and changes its attribute of completed from true/false. This is then saved in state of allTodos
function handleChange(id) {
const updatedTodos = allTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
setTodos(updatedTodos)
}
const todoComponents = allTodos.map(item => <Todos key={item.id} item={item} handleChange={handleChange}/>)
the function updateDB takes that value from state and using it to update the database.
function updateDB(event) {
event.preventDefault()
const value = {
completed: false,
text: newTodo,
id: allTodos.length,
}
}
Here's where the problem arises: id: allTodos.length. If one of these are deleted, it will create a todo with a duplicate ID, crashing the whole thing. I don't know how to avoid this problem.
In updateDB, you are setting id to allTodos.length aka 1.

How to properly manage the order data is received and DOM updated in React Hooks

I am using React Hooks and I want to use useMemo to trigger rerenders if and only if certain data changes.
i am also using apollo and I have a useQuery graphql query that I want to refresh under certain circumstances. The problem is, I'm getting some odd behaviour - sometimes the data just doesn't load. Also sometimes the context is not ready and no variables are passed to the useQuery.
My useMemo hook seems like it's repeating code, but getting rid of any of these if statements breaks the page.
Here is my functional component:
function Accounts(props) {
const userContext = useContext(UserContext)
const accountContext = useContext(AccountContext)
const parentAccountID = userContext.userState.accountId
const [parentAccountIDs, setParentAccountIDs] = useState(null)
const [vars, setVars] = useState({
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
});
const {
loading: loadingAccountUsers,
error: errorAccountUsers,
data: dataAccountUsers
} = useQuery(GET_ACCOUNT_USERS, {
variables: {
accountId: parentAccountID
}
})
const {
loading: loadingAccounts,
error: errorAccounts,
data: dataAccounts,
refetch: refetchAccounts
} = useQuery(GET_ACCOUNTS, {
variables: {vars}
})
const {
loading: loadingAccountsTotal,
error: errorAccountsTotal,
data: dataAccountsTotal,
refetch: refetchAccountsTotal
} = useQuery(TOTAL_ACCOUNTS, {
variables: {vars}
})
const setParentIDsHandler = (id) => {
setParentAccountIDs(String(id))
}
const setOffset = (offset, limit) => {
console.log("SET OFFSET", offset, limit)
accountContext.accountDispatch({
type: SET_ACCOUNTS_OFFSET,
payload: {
offset: offset,
limit: limit
}
})
setVars({
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
})
}
useMemo(() => {
if (dataAccountsTotal) {
accountContext.accountDispatch({type: SET_ACCOUNTS_TOTAL, payload: dataAccountsTotal})
console.log("TOTAL ACCOUNTS", accountContext.accountState.data.accountsTotal)
}
if (dataAccounts && dataAccountUsers && vars) {
refetchAccounts(vars)
accountContext.accountDispatch({type: GET_PARENT_ACCOUNT, payload: dataAccountUsers})
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: dataAccounts})
setParentIDsHandler(dataAccountUsers.accountUsers[0].account.id)
}
}, [
dataAccounts,
dataAccountsTotal,
dataAccountUsers,
errorAccounts,
loadingAccounts,
parentAccountIDs,
vars
])
return (
<Fragment>
{
accountContext.accountState.data.accounts &&
!loadingAccountUsers &&
!errorAccountUsers &&
!loadingAccounts &&
!errorAccounts &&
parentAccountIDs &&
accountContext.accountState.data.accountUsers
? <AccountsGrid setOffset={setOffset}/> : <SpinnerLoader />}
</Fragment>
)
}
export default Accounts
When my handler function is called, sometimes the data being passed to the AccountsGrid component gets out of sync with the data from the server. How can I force React to wait for the data and only show it when it's available?

Resources