fix(ServiceRequest): падение просмотра заявки — хук после раннего return

useMemo с attachmentUrls стоял ПОСЛЕ `if (loading) return` — на первом
рендере (загрузка) он не вызывался, на втором вызывался, порядок хуков
менялся, и React ронял компонент с #310 «rendered more hooks than during
the previous render». Карточка заявки на услуги не открывалась вообще.
Перенёс расчёт выше ранних выходов.

Заодно включил `react-hooks/rules-of-hooks` (error) — плагин давно в
зависимостях и в коде есть eslint-disable для exhaustive-deps, то есть
правила подразумевались, но в .eslintrc их забыли объявить. По остальному
коду правило даёт 0 нарушений, так что этот баг был единственным.
`exhaustive-deps` пока оставил выключенным: на текущем коде это 16
предупреждений, их разбор — отдельная задача, а не довесок к фиксу.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
gorbenko.pavel
2026-09-01 16:49:44 +05:00
parent a3d3983d78
commit 1e265875e2
2 changed files with 29 additions and 9 deletions
+17
View File
@@ -11,7 +11,24 @@ module.exports = {
'ecmaVersion': 2018, 'ecmaVersion': 2018,
'sourceType': 'module' 'sourceType': 'module'
}, },
plugins: ['react-hooks'],
rules: { rules: {
// RATIONALE: Хук, вызванный не на каждом рендере (например, useMemo
// ПОСЛЕ раннего `return` для loading/error), ломает порядок
// хуков и валит компонент в рантайме с React #310 «rendered
// more hooks than during the previous render» — ошибка,
// которую видно только при открытии конкретного экрана.
// Правило ловит это на сборке. eslint-plugin-react-hooks
// давно в зависимостях, а в коде есть eslint-disable для
// exhaustive-deps — то есть правила подразумевались, но
// включить их в конфиг забыли (поймали на ServiceRequestView).
'react-hooks/rules-of-hooks': 2,
// Зависимости эффектов — ВЫКЛЮЧЕНО намеренно. Правило полезное, но на
// текущем коде даёт 16 предупреждений (в основном намеренно опущенные
// onCodeLoaded/submit во всех формах). Шум такого объёма в каждой сборке
// приучает не читать варнинги вовсе — включать имеет смысл вместе с
// разбором этих мест, отдельной задачей, а не заодно с фиксом падения.
'react-hooks/exhaustive-deps': 0,
// Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin // Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin
'@rushstack/no-new-null': 1, '@rushstack/no-new-null': 1,
// Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin // Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin
@@ -107,6 +107,18 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
}; };
}, [itemId, listId]); }, [itemId, listId]);
// Ссылки на скачивание для файлов строк «Приложения» (см. AnnexesSection).
// Считаем ДО ранних return'ов ниже: хук, вызванный только на части рендеров,
// ломает порядок хуков (React #310 — «rendered more hooks than during the
// previous render»), потому что на первом рендере (loading) его бы не было.
const attachmentUrls = useMemo(() => {
const map: Record<string, string> = {};
attachments.forEach((a) => {
map[a.fileName] = a.url;
});
return map;
}, [attachments]);
if (loading) { if (loading) {
return <div className={styles.state}>Загрузка заявки…</div>; return <div className={styles.state}>Загрузка заявки…</div>;
} }
@@ -118,15 +130,6 @@ const ServiceRequestView: React.FC<IServiceRequestViewProps> = ({ context, itemI
); );
} }
// Ссылки на скачивание для файлов строк «Приложения» (см. AnnexesSection).
const attachmentUrls = useMemo(() => {
const map: Record<string, string> = {};
attachments.forEach((a) => {
map[a.fileName] = a.url;
});
return map;
}, [attachments]);
const sectionProps = { data, set, context, printOrgIndex, attachmentUrls }; const sectionProps = { data, set, context, printOrgIndex, attachmentUrls };
const notify = (): void => { const notify = (): void => {