From cc85b3b337e5beed1686cbd4728ec2ddef37cd99 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Mon, 25 May 2026 18:13:56 +0300 Subject: [PATCH] =?UTF-8?q?fix(admin-ui):=20UserPicker=20=E2=80=94=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20=D1=85=D1=83=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2=20(rules-of-hooks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Early-return на TextInput-fallback стоял ДО useMemo(triggerLabel) и useMemo(selected). React падает «rendered fewer hooks than expected» когда количество вызовов хуков меняется между рендерами. Reproduce: loading=true первый рендер → ВСЕ хуки выполнились (включая triggerLabel). loading=false с data=[] второй рендер → fallback возвращается до triggerLabel useMemo → меньше хуков → error boundary ловит → юзер видит «Что-то пошло не так». Перенёс все useMemo выше early-return. Логика рендера без изменений. --- .../src/components/audit/UserPicker.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ordinis-admin-ui/src/components/audit/UserPicker.tsx b/ordinis-admin-ui/src/components/audit/UserPicker.tsx index d6a0f17..4b93a34 100644 --- a/ordinis-admin-ui/src/components/audit/UserPicker.tsx +++ b/ordinis-admin-ui/src/components/audit/UserPicker.tsx @@ -46,11 +46,23 @@ export function UserPicker({ value, onChange, label, placeholder, disabled }: Us const fallbackToTextInput = !usersQ.isLoading && !usersQ.isError && usersList.length === 0 + // Все хуки ДО любого early-return — иначе rules-of-hooks нарушится при + // переходе loading→empty (loading rendered ВСЕ хуки → empty возвращает + // TextInput минуя последующие useMemo). React в production падает на + // "rendered fewer hooks than expected" → error boundary → "Что-то пошло + // не так". Reproduce: loading=true первый рендер, loading=false data=[] + // второй. const usersBySub = useMemo(() => { const m = new Map() for (const u of usersList) m.set(u.sub, u) return m }, [usersList]) + const selected = value ? usersBySub.get(value) : undefined + const triggerLabel = useMemo(() => { + if (!value) return placeholder ?? 'Любой' + if (selected) return labelFor(selected) + return `${value.slice(0, 8)}…` + }, [value, selected, placeholder]) if (fallbackToTextInput) { return ( @@ -65,14 +77,6 @@ export function UserPicker({ value, onChange, label, placeholder, disabled }: Us ) } - const selected = value ? usersBySub.get(value) : undefined - const triggerLabel = useMemo(() => { - if (!value) return placeholder ?? 'Любой' - if (selected) return labelFor(selected) - // Value есть, но юзера нет в кеше — показываем UUID prefix. - return `${value.slice(0, 8)}…` - }, [value, selected, placeholder]) - // Поиск: substring по preferredUsername / name / email / sub. cmdk // делает client-side фильтрацию по value атрибуту CommandItem'а; // концатенируем все поля в один searchable string.