diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f55..3489436c44fb 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -23,11 +23,25 @@ import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer'; import {getAdditionalEffectHooksFromSettings} from '../shared/Utils'; /** - * Catch all identifiers that begin with "use" followed by an uppercase Latin + * Catch all identifiers that begin with "use" followed by an uppercase * character to exclude identifiers like "user". + * + * We use toUpperCase() comparison to support Unicode letters, not just A-Z. */ function isHookName(s: string): boolean { - return s === 'use' || /^use[A-Z0-9]/.test(s); + if (s === 'use') { + return true; + } + if (s.length < 4 || !s.startsWith('use')) { + return false; + } + const fourthChar = s[3]; + // Check if it's a digit or an uppercase letter (including Unicode) + const isDigit = fourthChar >= '0' && fourthChar <= '9'; + const isUpperCaseLetter = + fourthChar === fourthChar.toUpperCase() && + fourthChar !== fourthChar.toLowerCase(); + return isDigit || isUpperCaseLetter; } /** @@ -43,8 +57,15 @@ function isHook(node: Node): boolean { isHook(node.property) ) { const obj = node.object; - const isPascalCaseNameSpace = /^[A-Z].*/; - return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name); + // Check if namespace starts with uppercase letter (including Unicode) + if (obj.type === 'Identifier') { + const firstChar = obj.name[0]; + return ( + firstChar === firstChar.toUpperCase() && + firstChar !== firstChar.toLowerCase() + ); + } + return false; } else { return false; } @@ -53,9 +74,17 @@ function isHook(node: Node): boolean { /** * Checks if the node is a React component name. React component names must * always start with an uppercase letter. + * + * We use toUpperCase() comparison to support Unicode letters, not just A-Z. + * This allows component names like "ÄndraVärde" to be recognized as valid + * React components. */ function isComponentName(node: Node): boolean { - return node.type === 'Identifier' && /^[A-Z]/.test(node.name); + return ( + node.type === 'Identifier' && + node.name[0] === node.name[0].toUpperCase() && + node.name[0] !== node.name[0].toLowerCase() + ); } function isReactFunction(node: Node, functionName: string): boolean { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index d055b271ad77..bfbf1404457f 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1120,7 +1120,12 @@ export function performWorkOnRoot( forceSync: boolean, ): void { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - throw new Error('Should not already be working.'); + // There's already work running on this stack. This can happen + // in Firefox when an alert/debugger/confirm blocks the main thread + // but a message event still fires, causing a reentrant render. + // Instead of throwing, we exit early and let the original render + // finish and reschedule a new render when it's done. + return; } if (enableProfilerTimer && enableComponentPerformanceTrack) { @@ -3512,7 +3517,12 @@ function completeRoot( flushRenderPhaseStrictModeWarningsInDEV(); if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { - throw new Error('Should not already be working.'); + // There's already work running on this stack. This can happen + // in Firefox when an alert/debugger/confirm blocks the main thread + // but a message event still fires, causing a reentrant commit. + // Instead of throwing, we exit early and let the original commit + // finish when the user dismisses the modal. + return; } if (enableProfilerTimer && enableComponentPerformanceTrack) {