Review

Query-backed climbing gym member profile

Front desk still hammers /members/:id on every tab focus because the profile page uses ad-hoc useEffect fetch. Swap it for useQuery with a 60s staleTime so badge scans feel instant, and wrap the page in QueryErrorResetBoundary so a flaky members API shows a retry instead of a blank card.

React QueryTier 1react-queryprofileerror-boundary

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

pages/members/MemberProfilePage.tsx+30-16
11import { useParams } from "react-router-dom";
2-import { useEffect, useState } from "react";
2+import { QueryErrorResetBoundary, useQuery } from "@tanstack/react-query";
3+import { ErrorBoundary } from "react-error-boundary";
34import { fetchMemberProfile } from "@/api/members";
45import { MemberCard } from "@/components/members/MemberCard";
56
67export function MemberProfilePage() {
78 const { memberId } = useParams<{ memberId: string }>();
8- const [member, setMember] = useState<MemberProfile | null>(null);
9- const [loading, setLoading] = useState(true);
10-
11- useEffect(() => {
12- if (!memberId) return;
13- setLoading(true);
14- fetchMemberProfile(memberId).then((m) => {
15- setMember(m);
16- setLoading(false);
17- });
18- }, [memberId]);
19-
20- if (loading) return <ProfileSkeleton />;
21- if (!member) return null;
22- return <MemberCard member={member} />;
9+
10+ return (
11+ <QueryErrorResetBoundary>
12+ {({ reset }) => (
13+ <ErrorBoundary
14+ onReset={reset}
15+ fallbackRender={({ resetErrorBoundary }) => (
16+ <button type="button" onClick={resetErrorBoundary}>Retry profile</button>
17+ )}
18+ >
19+ <MemberProfileBody memberId={memberId!} />
20+ </ErrorBoundary>
21+ )}
22+ </QueryErrorResetBoundary>
23+ );
2324}
25+
26+function MemberProfileBody({ memberId }: { memberId: string }) {
27+ // Profile rarely changes mid-shift; avoid refetch-on-focus with staleTime: 0.
28+ const { data: member, isPending } = useQuery({
29+ queryKey: ["member-profile", memberId],
30+ queryFn: () => fetchMemberProfile(memberId),
31+ staleTime: 60_000,
32+ throwOnError: true,
33+ });
34+
35+ if (isPending) return <ProfileSkeleton />;
36+ return <MemberCard member={member!} />;
37+}