From 63afd2fc89ff86cba2b94a0a7b995f3ef20a65c5 Mon Sep 17 00:00:00 2001 From: devsyncwrld Date: Mon, 31 Aug 2026 12:12:23 -0300 Subject: [PATCH] fix(profile): hoist the activity hook above the early return MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/modals/UserProfileModal.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/web/src/components/modals/UserProfileModal.tsx b/packages/web/src/components/modals/UserProfileModal.tsx index c5b6c7f5..1d75f413 100644 --- a/packages/web/src/components/modals/UserProfileModal.tsx +++ b/packages/web/src/components/modals/UserProfileModal.tsx @@ -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))