-
Notifications
You must be signed in to change notification settings - Fork 2
Update Pages data with SWR #434
base: bezos-main
Are you sure you want to change the base?
Conversation
* List projects over full width * Center control buttons in Top Nav * Give LanguageButton same size on small screens
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// when carbonmark user data changed | ||
useEffect(() => { | ||
// store first data and return | ||
if (!initialUser.current && carbonmarkUser) { | ||
initialUser.current = carbonmarkUser; | ||
return; | ||
} | ||
|
||
const fetchUser = () => | ||
getUser({ | ||
user: props.userAddress, | ||
type: "wallet", | ||
}); | ||
// when activity data differs => reset state and update initial user | ||
if ( | ||
initialUser.current && | ||
carbonmarkUser && | ||
getActivityIsUpdated(initialUser.current, carbonmarkUser) | ||
) { | ||
setInterval(0); | ||
setIsUpdatingUser(false); | ||
initialUser.current = carbonmarkUser; | ||
fetchIntervalTimer.current && clearTimeout(fetchIntervalTimer.current); | ||
} | ||
}, [carbonmarkUser]); | ||
|
||
// API is updated when new activity exists | ||
const activityIsAdded = (value: User) => { | ||
const newActivityLength = value.activities.length; | ||
const currentActivityLength = user.activities.length; | ||
return newActivityLength > currentActivityLength; | ||
}; | ||
// on unmount | ||
useEffect(() => { | ||
return () => { | ||
fetchIntervalTimer.current && clearTimeout(fetchIntervalTimer.current); | ||
}; | ||
}, []); | ||
|
||
const updatedUser = await pollUntil({ | ||
fn: fetchUser, | ||
validate: activityIsAdded, | ||
ms: 1000, | ||
maxAttempts: 50, | ||
}); | ||
const onUpdateUser = async () => { | ||
// increase fetch interval | ||
// the useEffect above will take care of updated carbonmark user data | ||
setInterval(1000); | ||
setIsUpdatingUser(true); | ||
|
||
setUser((prev) => ({ ...prev, ...updatedUser })); | ||
} catch (e) { | ||
console.error("LOAD USER ACTIVITY error", e); | ||
// Show error message after 50 seconds | ||
fetchIntervalTimer.current = setTimeout(() => { | ||
setInterval(0); | ||
setIsUpdatingUser(false); | ||
setErrorMessage( | ||
t`Please refresh the page. There was an error updating your data: ${e}.` | ||
t`Please refresh the page. There was an error updating your data.` | ||
); | ||
} finally { | ||
setIsUpdatingUser(false); | ||
} | ||
}, 50000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also be removed if we update useFetchUser
with the timeout arg
export const getActivityIsUpdated = (oldUser: User, newUser: User) => { | ||
const formerActivityLength = oldUser?.activities?.length || 0; | ||
const newActivityLength = newUser?.activities?.length || 0; | ||
return newActivityLength > formerActivityLength; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably isn't needed if you're no longer managing two user states
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion!
I was struggling here with SWR because what I want to achieve is this:
Under a certain condition => start fetching until the resource is updated with fresh data
I thought SWR would handle this for me:
Fetch again and again and validate if the data is updated
I can trigger one fetch, yes.
But what if the backend needs longer?
Sometimes even 50 seconds to be updated?
SWR did not tell me how to achieve this. There is this option "compare(a, b)" in the API docs but this did not work.
So what I went for is this:
- When a user contract transaction was successful => increase the SWR fetch interval for the user backend endpoint
- compare the former user data with the new incoming data (every XY second)
- When the new user data contains new activities => backend is done! Decrease SWR fetch interval
- If the SWR fetch interval takes too long (backend needs forever) => timeout! Decrease the interval and show an error message
SWR, what am I missing?
Any idea on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok I misunderstood your intention.
If I understand correctly, you'd like an increasing fetch interval with multiple errors until the response contains some condition? (number of activities > 0 for example) That makes sense.
SWR
uses an exponential back-off algorithm on errors (see error-retry) so the behaviour you're looking for should come out of the box.
We can leverage this with something like:
export class NoUserActivitiesError extends Error {
public user: User;
constructor(user: User) {
super("User has no activities");
this.name = "NoActivitiesError";
this.user = user;
}
}
// Fetches a user from the Carbonmark API
export const useFetchUser = (address?: string, options?: SWRConfiguration) => {
const { data, ...rest } = useSWR<User, Error>(
address ? `/api/users/${address}?type=wallet` : null,
async (url) => {
const user: User = await fetcher(url);
if (user.activities.length === 0) {
throw new NoUserActivitiesError(user);
}
return user;
},
{
...options,
errorRetryCount: 5,
}
);
const carbonmarkUser = !!data?.handle ? data : null;
return { carbonmarkUser, ...rest };
};
You can now use the errorRetryCount
to determine how many times swr will attempt to refetch within the default timeout time (the time set by the browser by default).
Does this achieve what you're after? It would be preferable to duplicating code and managing multiple states and timeouts etc (for those devs that need to work on this in future)
One thing to consider is also what if the user genuinely has no activities? I think this may be an issue with the API where by it should return nothing until it has resolved activities (rather than return an empty array)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I will give this a try.
Just for completion:
Checking for simply > 0 is not enough.
The condition is met when the new user's activity length is higher than the former one.
So we will always need the former data for comparison.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry so you have a pending fetch for increases to the number of activities? Is there a reason why invalidating the swr key (triggering a refetch) on any action would not achieve this sync that you're after? The query should also invalidate on tab switch etc so that should be covered.
This discussion is unrelated to this PR and just raised again here which is not the correct place Please continue the discussion in this ticket here: |
* change project card * check project price before passing to formatBigToPrice fn * update project id
b91b184
to
9cb777f
Compare
Oh I see, so the issue is that looking at my own profile page looks very similar to looking at the user page for my account? And logging out while on my profile will take me to my user page (users/0xemc)? I can see this causing some confusion with users @jabby09 perhaps logging out should take the user to Anyway @ladytrekker if that's what's intended then fair enough I'll close this once jabby replies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an idea that will be a bit more reuseable, and hopefully less complex as it won't need useEffect or refs.
I believe this can be done by awaiting an async function that contains all the retry logic.
Something like this?
const { mutate, data: oldUser } = useSWR("/api/user/ladytrekker");
const handleMutateUser = async () => {
// start loading spinners
setPending(true);
// retry until...
const newUser = await retryFetchUser(
(newUser) => newUser.activities.length > oldUser.activities.length
);
setPending(false);
// I'm passing in data here to simulate that we got a fresh record from API
return newUser;
};
const onUpdateUser = async () => {
// optional: we can create a synthetic activity and not show the loading spinners
const optimisticData = { ...oldUser, activities: newActivities };
mutate(handleMutateUser, {
revalidate: false,
populateCache: true,
optimisticData, // optional
});
};
I created a demo here that shows the persistent data, including mutating data on screens that haven't mounted yet: https://sxddxw-3000.csb.app/
import { fetcher } from "lib/fetcher"; | ||
import type { SWRConfiguration } from "swr"; | ||
import useSWR from "swr"; | ||
import { Project } from "../lib/types/carbonmark"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { Project } from "../lib/types/carbonmark"; | |
import { Project } from "lib/types/carbonmark"; |
import { fetcher } from "lib/fetcher"; | ||
import type { SWRConfiguration } from "swr"; | ||
import useSWR from "swr"; | ||
import { User } from "../lib/types/carbonmark"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { User } from "../lib/types/carbonmark"; | |
import { User } from "lib/types/carbonmark"; |
@@ -14,15 +14,16 @@ interface Params extends ParsedUrlQuery { | |||
user: string; | |||
} | |||
|
|||
interface PageProps { | |||
export interface UserPageStaticProps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it a circular dependency when this is used to define the Page component, but Page component is imported here? Maybe best to export from the page component...
@0xemc @ladytrekker yes I agree it is confusing, I raised this during dev but it was low priority and we had bigger UX issues to solve :) I think if you are on your profile page logged in (https://www.carbonmark.com/users/jabby) and I click the LOG OUT button, I should be redirected to https://www.carbonmark.com/users/login (note: this is the behaviour on the Portfolio page) |
This flow doesn't seem to work well when editing a listing:
"edit 3" disappears when refreshing (one time), and also when switching pages and coming back to Profile. I haven't tested other flows yet. Per @Atmosfearful , the firebase env variable hasn't been swapped for staging to point to the mainnet one. I just want to mention this in case it's relevant to the problem, since I had to create a new profile. |
Description
This PR integrates SWR to the following pages
With this integration, the following caching "bugs" related to NextJS page rendering behaviour are FIXED:
Or
All this should be fixed now ✅
Please test this PR heavily 🙏 Thanks!
Related Ticket
Closes https://github.com/Atmosfearful/bezos-frontend/issues/357
Changes
Another tiny change in this PR:
When a project can not be found
=> the page is not rendered no more! Instead the request fails with a 404
Before: https://www.carbonmark.com/projects/not-a-project
After: https://carbonmark-git-ladytrekker-swr-klimadao.vercel.app/projects/not-a-project
Checklist
npm run build-all
without errorsnpm run format-all