diff --git a/eslint.config.mjs b/eslint.config.mjs index e44e4105..73d03b69 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,7 @@ const eslintConfig = defineConfig([ rules: { // Temporarily disable during Next.js 16 migration 'react-hooks/refs': 'off', - 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/set-state-in-effect': 'warn', '@next/next/no-img-element': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-require-imports': 'off', diff --git a/src/app/AppState.ts b/src/app/AppState.ts index 9e3b0305..a42f01cb 100644 --- a/src/app/AppState.ts +++ b/src/app/AppState.ts @@ -30,6 +30,8 @@ export type AppStateContextType = { categoriesWithCounts?: Awaited> + // ENVIRONMENT + supportsHover?: boolean // MODAL isCommandKOpen?: boolean setIsCommandKOpen?: Dispatch> diff --git a/src/app/AppStateProvider.tsx b/src/app/AppStateProvider.tsx index c7047eaf..012c9310 100644 --- a/src/app/AppStateProvider.tsx +++ b/src/app/AppStateProvider.tsx @@ -40,6 +40,7 @@ import { SWRKey, } from '@/swr'; import { warmRedisAction } from './actions'; +import useSupportsHover from '@/utility/useSupportsHover'; export default function AppStateProvider({ children, @@ -78,6 +79,8 @@ export default function AppStateProvider({ }, [nextPhotoAnimationId, setNextPhotoAnimation]); const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] = useState(true); + // ENVIRONMENT + const supportsHover = useSupportsHover(); // MODAL const [isCommandKOpen, setIsCommandKOpen] = useState(false); @@ -229,6 +232,8 @@ export default function AppStateProvider({ shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands, categoriesWithCounts, + // ENVIRONMENT + supportsHover, // MODAL isCommandKOpen, setIsCommandKOpen, diff --git a/src/components/primitives/TooltipPrimitive.tsx b/src/components/primitives/TooltipPrimitive.tsx index d3d79fb0..4d7c652f 100644 --- a/src/components/primitives/TooltipPrimitive.tsx +++ b/src/components/primitives/TooltipPrimitive.tsx @@ -3,11 +3,11 @@ import { ReactNode, useRef, useState, ComponentProps } from 'react'; import * as Tooltip from '@radix-ui/react-tooltip'; import MenuSurface from './MenuSurface'; -import useSupportsHover from '@/utility/useSupportsHover'; import clsx from 'clsx/lite'; import useClickInsideOutside from '@/utility/useClickInsideOutside'; import KeyCommand from './KeyCommand'; import { clearGlobalFocus } from '@/utility/dom'; +import { useAppState } from '@/app/AppState'; export default function TooltipPrimitive({ content: contentProp, @@ -51,7 +51,7 @@ export default function TooltipPrimitive({ const [isOpen, setIsOpen] = useState(false); - const supportsHover = useSupportsHover(); + const { supportsHover } = useAppState(); const includeButton = supportMobile && supportsHover === false; diff --git a/src/components/shared-hover/SharedHover.tsx b/src/components/shared-hover/SharedHover.tsx index 2b984e15..58afb9e7 100644 --- a/src/components/shared-hover/SharedHover.tsx +++ b/src/components/shared-hover/SharedHover.tsx @@ -2,8 +2,8 @@ import { ReactNode, useRef, useEffect } from 'react'; import { SharedHoverProps, useSharedHoverState } from '../shared-hover/state'; -import useSupportsHover from '@/utility/useSupportsHover'; import clsx from 'clsx/lite'; +import { useAppState } from '@/app/AppState'; export default function SharedHover({ hoverKey: key, @@ -28,6 +28,8 @@ export default function SharedHover({ }) { const ref = useRef(null); + const { supportsHover } = useAppState(); + const { showHover, dismissHover, @@ -37,8 +39,6 @@ export default function SharedHover({ const isHovering = isHoverBeingShown?.(key); - const supportsHover = useSupportsHover(); - useEffect(() => { const trigger = ref.current; return () => dismissHover?.(trigger); diff --git a/src/utility/useSupportsHover.ts b/src/utility/useSupportsHover.ts index e0d3ad8b..0454e178 100644 --- a/src/utility/useSupportsHover.ts +++ b/src/utility/useSupportsHover.ts @@ -1,18 +1,18 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; export default function useSupportsHover() { - const [supportsHover, setSupportsHover] = useState(); + const mqlRef = useRef(typeof window !== 'undefined' + ? window.matchMedia('(hover: hover)') + : undefined); + + const [supportsHover, setSupportsHover] = + useState(mqlRef.current?.matches ?? false); useEffect(() => { - const mql = window.matchMedia('(hover: hover)'); - - setSupportsHover(mql.matches); - const listener = (e: MediaQueryListEvent) => { - setSupportsHover(e.matches); - }; - - mql.addEventListener('change', listener); - return () => mql.removeEventListener('change', listener); + const listener = (e: MediaQueryListEvent) => setSupportsHover(e.matches); + const mql = mqlRef.current; + mql?.addEventListener('change', listener); + return () => mql?.removeEventListener('change', listener); }, []); return supportsHover;