Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -574,14 +574,24 @@ function codegenReactiveScope(

for (const dep of [...scope.dependencies].sort(compareScopeDependency)) {
const index = cx.nextCacheIndex;
const comparison = t.binaryExpression(
'!==',
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
// Use Object.is semantics for dependency comparison to handle NaN correctly
// Object.is(NaN, NaN) is true, NaN !== NaN is true, so we need !Object.is(a, b)
const comparison = t.unaryExpression(
'!',
t.callExpression(
t.memberExpression(
t.identifier('Object'),
t.identifier('is'),
),
[
t.memberExpression(
t.identifier(cx.synthesizeName('$')),
t.numericLiteral(index),
true,
),
codegenDependency(cx, dep),
],
),
codegenDependency(cx, dep),
);
changeExpressions.push(comparison);
/*
Expand Down
22 changes: 10 additions & 12 deletions packages/eslint-plugin-react-hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,16 @@ const configs = {
plugins,
rules: recommendedLatestRuleConfigs,
},
flat: {} as {
flat: {
recommended: {
plugins: {'react-hooks': plugin},
rules: recommendedRuleConfigs,
},
'recommended-latest': {
plugins: {'react-hooks': plugin},
rules: recommendedLatestRuleConfigs,
},
} satisfies {
recommended: ReactHooksFlatConfig;
'recommended-latest': ReactHooksFlatConfig;
},
Expand All @@ -86,15 +95,4 @@ const plugin = {
configs,
};

Object.assign(configs.flat, {
'recommended-latest': {
plugins: {'react-hooks': plugin},
rules: configs['recommended-latest'].rules,
},
recommended: {
plugins: {'react-hooks': plugin},
rules: configs.recommended.rules,
},
});

export default plugin;
39 changes: 34 additions & 5 deletions packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}
Expand All @@ -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 {
Expand Down
14 changes: 12 additions & 2 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down