diff --git a/.changeset/slimy-ways-joke.md b/.changeset/slimy-ways-joke.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/slimy-ways-joke.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/eslint.config.mjs b/eslint.config.mjs index 066aa4a553c..d1dc74af297 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -586,6 +586,19 @@ export default tseslint.config([ rules: { ...pluginReactHooks.configs.recommended.rules, 'react-hooks/rules-of-hooks': 'warn', + // Turned off while upgrading eslint-plugin-react-hooks to v7, which added these to the recommended preset. + // They are not meant to stay off; they will be enabled in follow-up changes. + 'react-hooks/immutability': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/set-state-in-effect': 'off', + }, + }, + { + name: 'repo/react-hooks/test', + files: TEST_FILES, + rules: { + 'react-hooks/globals': 'off', }, }, ...PACKAGE_IMPORT_RESOLVER_CONFIGS, diff --git a/package.json b/package.json index c6a6a5ab9bd..52c667c6efc 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-playwright": "2.10.1", "eslint-plugin-react": "7.37.5", - "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-hooks": "7.1.1", "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-sonarjs": "4.2.0", "eslint-plugin-turbo": "2.5.5", diff --git a/packages/astro/src/react/hooks.ts b/packages/astro/src/react/hooks.ts index 6ed2d878598..b6c86959cf1 100644 --- a/packages/astro/src/react/hooks.ts +++ b/packages/astro/src/react/hooks.ts @@ -11,7 +11,7 @@ import type { UseAuthReturn, } from '@clerk/shared/types'; import type { Store, StoreValue } from 'nanostores'; -import { useCallback, useSyncExternalStore } from 'react'; +import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { authAsyncStorage } from '#async-local-storage'; @@ -88,8 +88,8 @@ type UseAuth = (options?: PendingSessionOptions) => UseAuthReturn; export const useAuth: UseAuth = ({ treatPendingAsSignedOut } = {}) => { const authContext = useAuthStore(); - const getToken: GetToken = useCallback(createGetToken(), []); - const signOut: SignOut = useCallback(createSignOut(), []); + const getToken: GetToken = useMemo(() => createGetToken(), []); + const signOut: SignOut = useMemo(() => createSignOut(), []); const { userId, orgId, orgRole, orgPermissions, factorVerificationAge, sessionClaims } = authContext; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 4957f7b6796..440ddcc0390 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -8,7 +8,7 @@ import type { SignOut, UseAuthReturn, } from '@clerk/shared/types'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { errorThrower } from '../errors/errorThrower'; @@ -99,8 +99,8 @@ export const useAuth = (options: UseAuthOptions = {}): UseAuthReturn => { const authState = useAuthBase(); const isomorphicClerk = useIsomorphicClerkContext(); - const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); - const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]); + const getToken: GetToken = useMemo(() => createGetToken(isomorphicClerk), [isomorphicClerk]); + const signOut: SignOut = useMemo(() => createSignOut(isomorphicClerk), [isomorphicClerk]); isomorphicClerk.telemetry?.record(eventMethodCalled('useAuth', { treatPendingAsSignedOut })); diff --git a/packages/shared/src/react/hooks/useDeepEqualMemo.ts b/packages/shared/src/react/hooks/useDeepEqualMemo.ts index fa96a84d221..157aa4a631c 100644 --- a/packages/shared/src/react/hooks/useDeepEqualMemo.ts +++ b/packages/shared/src/react/hooks/useDeepEqualMemo.ts @@ -17,6 +17,7 @@ const useDeepEqualMemoize = (value: T) => { * @internal */ export const useDeepEqualMemo: UseDeepEqualMemo = (factory, dependencyArray) => { + // eslint-disable-next-line react-hooks/use-memo -- the dependency list is deep-equal memoized so it is only replaced when its contents change return React.useMemo(factory, useDeepEqualMemoize(dependencyArray)); }; diff --git a/packages/shared/src/react/hooks/useOrganizationDirectorySync.shared.ts b/packages/shared/src/react/hooks/useOrganizationDirectorySync.shared.ts index 486b2a1b057..aaee513507c 100644 --- a/packages/shared/src/react/hooks/useOrganizationDirectorySync.shared.ts +++ b/packages/shared/src/react/hooks/useOrganizationDirectorySync.shared.ts @@ -37,6 +37,7 @@ export function useOrganizationDirectorySyncUsersCacheKeys(params: { args: GetDirectorySyncUsersParams; }) { const { organizationId, enterpriseConnectionId, directoryId, args } = params; + const argsKey = JSON.stringify(args); return useMemo(() => { return createCacheKeys({ stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_DIRECTORY_SYNC_USERS_KEY, @@ -52,7 +53,7 @@ export function useOrganizationDirectorySyncUsersCacheKeys(params: { }); // The args object is intentionally serialized via the consumer to keep stability. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [organizationId, enterpriseConnectionId, directoryId, JSON.stringify(args)]); + }, [organizationId, enterpriseConnectionId, directoryId, argsKey]); } /** diff --git a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts index 08690c8d7d4..98b8b47684d 100644 --- a/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts +++ b/packages/shared/src/react/hooks/useOrganizationEnterpriseConnectionTestRuns.shared.ts @@ -13,6 +13,7 @@ export function useOrganizationEnterpriseConnectionTestRunsCacheKeys(params: { args: GetEnterpriseConnectionTestRunsParams; }) { const { organizationId, enterpriseConnectionId, args } = params; + const argsKey = JSON.stringify(args); return useMemo(() => { return createCacheKeys({ stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY, @@ -27,5 +28,5 @@ export function useOrganizationEnterpriseConnectionTestRunsCacheKeys(params: { }); // The args object is intentionally serialized via the consumer to keep stability. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [organizationId, enterpriseConnectionId, JSON.stringify(args)]); + }, [organizationId, enterpriseConnectionId, argsKey]); } diff --git a/packages/ui/src/components/APIKeys/CreateAPIKeyForm.tsx b/packages/ui/src/components/APIKeys/CreateAPIKeyForm.tsx index 4b2caa80c1c..fa88b763477 100644 --- a/packages/ui/src/components/APIKeys/CreateAPIKeyForm.tsx +++ b/packages/ui/src/components/APIKeys/CreateAPIKeyForm.tsx @@ -146,6 +146,7 @@ export const CreateAPIKeyForm: React.FC = ({ onCreate }) return t(localizationKeys('apiKeys.formFieldCaption__expiration__never')); } + // eslint-disable-next-line react-hooks/purity -- the caption is memoized on the selected expiration, so it is computed from the time of selection const expirationDate = new Date(Date.now() + timeLeftInSeconds * 1000); return t( localizationKeys('apiKeys.formFieldCaption__expiration__expiresOn', { diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx index c687f4c7d3b..f63991b5aff 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx @@ -78,6 +78,7 @@ export const ConfigureProviderStep = (): JSX.Element | null => { elementId={descriptors.configureSSOStep.setId('configure')} > {ConfigureSteps ? ( + // eslint-disable-next-line react-hooks/static-components -- resolveConfigureSteps returns a module-level component, so its identity is stable across renders ) : ( <> diff --git a/packages/ui/src/components/devPrompts/KeylessPrompt/use-revalidate-environment.ts b/packages/ui/src/components/devPrompts/KeylessPrompt/use-revalidate-environment.ts index 067e915e206..6e2433e95f9 100644 --- a/packages/ui/src/components/devPrompts/KeylessPrompt/use-revalidate-environment.ts +++ b/packages/ui/src/components/devPrompts/KeylessPrompt/use-revalidate-environment.ts @@ -12,10 +12,11 @@ const THROTTLE_DURATION_MS = 10 * 1000; */ function useRevalidateEnvironment() { const clerk = useClerk(); - const lastTouchTimestamp = useRef(Date.now()); + const lastTouchTimestamp = useRef(0); const [, forceUpdate] = useReducer(v => v + 1, 0); useEffect(() => { + lastTouchTimestamp.current = Date.now(); const controller = new AbortController(); window.addEventListener( 'focus', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 834361df66e..7b7b5601955 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,8 +237,8 @@ importers: specifier: 7.37.5 version: 7.37.5(eslint@9.31.0(jiti@2.7.0)) eslint-plugin-react-hooks: - specifier: 5.2.0 - version: 5.2.0(eslint@9.31.0(jiti@2.7.0)) + specifier: 7.1.1 + version: 7.1.1(eslint@9.31.0(jiti@2.7.0)) eslint-plugin-simple-import-sort: specifier: 12.1.1 version: 12.1.1(eslint@9.31.0(jiti@2.7.0)) @@ -8669,11 +8669,11 @@ packages: peerDependencies: eslint: '>=8.40.0' - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react-you-might-not-need-an-effect@1.0.2: resolution: {integrity: sha512-HgYol2zhH3KbnW9Q4FY/FcIINbsYVL6rwQESChqBC2s9SLWtw1wg05+J7SCrr3BfQpNY2ReYhf8xjpd2JhKJOQ==} @@ -8741,7 +8741,6 @@ packages: eslint@9.31.0: resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -9631,6 +9630,9 @@ packages: hermes-compiler@250829098.0.14: resolution: {integrity: sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + hermes-estree@0.29.1: resolution: {integrity: sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==} @@ -9643,6 +9645,9 @@ packages: hermes-estree@0.36.0: resolution: {integrity: sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==} + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hermes-parser@0.29.1: resolution: {integrity: sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==} @@ -15051,6 +15056,12 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -24142,9 +24153,16 @@ snapshots: eslint: 9.31.0(jiti@2.7.0) globals: 17.13.0 - eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@9.31.0(jiti@2.7.0)): dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 eslint: 9.31.0(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) + transitivePeerDependencies: + - supports-color eslint-plugin-react-you-might-not-need-an-effect@1.0.2(eslint@9.31.0(jiti@2.7.0)): dependencies: @@ -25485,6 +25503,8 @@ snapshots: hermes-compiler@250829098.0.14: {} + hermes-estree@0.25.1: {} + hermes-estree@0.29.1: {} hermes-estree@0.32.0: {} @@ -25493,6 +25513,10 @@ snapshots: hermes-estree@0.36.0: {} + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + hermes-parser@0.29.1: dependencies: hermes-estree: 0.29.1 @@ -32051,6 +32075,10 @@ snapshots: dependencies: zod: 3.25.76 + zod-validation-error@4.0.2(zod@4.6.5): + dependencies: + zod: 4.6.5 + zod@3.25.76: {} zod@4.6.5: {}