fix(profile): hoist the activity hook above the early return
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
The activity selector sat below `if (!isOpen || !user) return null`. `user` arrives asynchronously, so the hook ran on some renders and not others; React counts hooks per render and tore the tree down with error #310 as soon as a profile finished loading. Move it above the guard and let the selector tolerate a null user. Typecheck and the suite both passed with the bug in place — TypeScript cannot see hook order and nothing renders this modal across the null-to-loaded transition.
This commit is contained in:
@@ -144,19 +144,27 @@ export function UserProfileModal() {
|
||||
return () => document.removeEventListener('keydown', handleKey);
|
||||
}, [isOpen, closeModal]);
|
||||
|
||||
// Keyed by home id, matching every other activity consumer (ActivityPanel,
|
||||
// MemberSidebar), so federated users resolve to the same record.
|
||||
//
|
||||
// Must sit ABOVE the early return: `user` is null on the first render and
|
||||
// arrives asynchronously, so a hook below it runs on some renders and not
|
||||
// others — React counts hooks per render and aborts the tree (#310).
|
||||
// The `?? []` stays OUTSIDE the selector; building it inside would hand
|
||||
// zustand a fresh array reference every render and spin.
|
||||
const activityList = useActivityStore((s) =>
|
||||
user ? s.userActivities.get(user.homeUserId ?? user.id) : undefined,
|
||||
);
|
||||
|
||||
if (!isOpen || !user) return null;
|
||||
|
||||
const activities = activityList ?? [];
|
||||
|
||||
const { baseName, domain } = parseFederatedUsername(user.username);
|
||||
const displayName = user.displayName ?? baseName;
|
||||
|
||||
// Banner — use correct API client for remote users
|
||||
const profileApi = getApiForOrigin(userOrigin);
|
||||
// Keyed by home id, matching every other activity consumer (ActivityPanel,
|
||||
// MemberSidebar), so federated users resolve to the same record. The `?? []`
|
||||
// stays OUTSIDE the selector: building it inside would hand zustand a fresh
|
||||
// array reference every render and spin.
|
||||
const activityList = useActivityStore((s) => s.userActivities.get(user.homeUserId ?? user.id));
|
||||
const activities = activityList ?? [];
|
||||
|
||||
const bannerSrc = user.banner
|
||||
? (user.banner.startsWith('http') ? user.banner : profileApi.uploads.url(user.banner))
|
||||
|
||||
Reference in New Issue
Block a user