From 5ee97e0a9f48fea1b267947c93c0d29585b59a43 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 13:03:04 +0300 Subject: [PATCH] perf(admin): migrate AdminUserDetail loadUser to React Query (caching + cache key) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the 13-line manual fetch with a useQuery hook keyed on userId. loadUser is kept as a thin wrapper around userQuery.refetch() so all 25+ existing call sites in mutation handlers continue to work unchanged. Sync useEffects copy data → setUser, isFetching → setLoading, and isError → navigate, preserving the original 'load fails → redirect to /admin/users' behavior. Remaining 12 loaders (loadSyncStatus/loadTariffs/loadTickets/loadReferrals/loadPanelInfo/ loadNodeUsage/loadDevices/loadSubscriptionData/loadGifts/loadPromoGroups/loadTicketDetail/ loadRequestHistory) NOT migrated in this commit — some are parameterized, some chain via Promise.all in loadSubscriptionData; they need a dedicated refactor with functional testing across all 7 tabs and the mutation flows. This commit ships the lowest-risk, highest-leverage piece (userQuery has the most call sites). --- src/pages/AdminUserDetail.tsx | 52 +++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 3232837..91a464f 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; import i18n from '../i18n'; import { DEVICE_ALIAS_MAX_LENGTH } from '../constants/devices'; import { useCurrency } from '../hooks/useCurrency'; @@ -369,19 +370,43 @@ export default function AdminUserDetail() { const userId = id ? parseInt(id, 10) : null; - const loadUser = useCallback(async () => { - if (!userId) return; - try { - setLoading(true); - const data = await adminUsersApi.getUser(userId); - setUser(data); - } catch (error) { - console.error('Failed to load user:', error); + // React Query owns the main user fetch: caching across navigations + auto-loading + // state. loadUser is kept as a thin refetch wrapper so the 25+ mutation handlers + // that call `await loadUser()` after writes need no changes. + const userQuery = useQuery({ + queryKey: ['admin-user-detail', userId] as const, + queryFn: () => { + if (!userId) throw new Error('No userId'); + return adminUsersApi.getUser(userId); + }, + enabled: !!userId && !isNaN(userId), + }); + + useEffect(() => { + if (userQuery.data) setUser(userQuery.data); + }, [userQuery.data]); + + useEffect(() => { + setLoading(userQuery.isFetching); + }, [userQuery.isFetching]); + + useEffect(() => { + if (userQuery.isError) { + console.error('Failed to load user:', userQuery.error); navigate('/admin/users'); - } finally { - setLoading(false); } - }, [userId, navigate]); + }, [userQuery.isError, userQuery.error, navigate]); + + const loadUser = useCallback( + async () => { + await userQuery.refetch(); + }, + // userQuery.refetch is a stable function across renders; including the whole + // userQuery object would re-create loadUser on every render and cascade into + // useEffects that depend on it. + // eslint-disable-next-line react-hooks/exhaustive-deps + [userQuery.refetch], + ); const loadSyncStatus = useCallback(async () => { if (!userId) return; @@ -579,10 +604,9 @@ export default function AdminUserDetail() { useEffect(() => { if (!userId || isNaN(userId)) { navigate('/admin/users'); - return; } - loadUser(); - }, [userId, loadUser, navigate]); + // user data is auto-loaded by userQuery (enabled when userId is valid) + }, [userId, navigate]); // Load panel info when subscription changes (separate from mount to avoid redundant loadUser) useEffect(() => {