fix: resolve react-doctor errors (hooks order, reduced motion, key prop) (#105)

- Move useCallback hooks before early return in ReferencesDialog
  to fix conditional hook call violation (Rules of Hooks)
- Add useReducedMotion hook and apply to all motion animations
  for WCAG 2.3.3 accessibility compliance
- Replace useEffect state reset with key prop on ItemCatalog
  for proper React reconciliation on category change
- Add global prefers-reduced-motion CSS media query

Fixes 4 react-doctor errors, bringing score from 81 to ~85+.
This commit is contained in:
Anton
2026-02-19 19:20:05 +00:00
committed by GitHub
parent 9a0f83a1ec
commit ff85765728
5 changed files with 47 additions and 16 deletions
@@ -8,6 +8,7 @@ import { ControlModes } from "./control-modes";
import { PhaseSwitcher } from "./phase-switcher";
import { StructureTools } from "./structure-tools";
import useEditor from "@/store/use-editor";
import { useReducedMotion } from "@/hooks/use-reduced-motion";
import { AnimatePresence, motion } from "motion/react";
import { ItemCatalog } from "../item-catalog/item-catalog";
import { FurnishTools } from "./furnish-tools";
@@ -18,6 +19,8 @@ export function ActionMenu({ className }: { className?: string }) {
const mode = useEditor((state) => state.mode);
const tool = useEditor((state) => state.tool);
const catalogCategory = useEditor((state) => state.catalogCategory);
const reducedMotion = useReducedMotion();
const transition = reducedMotion ? { duration: 0 } : undefined;
return (
<TooltipProvider>
@@ -57,8 +60,9 @@ export function ActionMenu({ className }: { className?: string }) {
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<ItemCatalog category={catalogCategory} />
<ItemCatalog key={catalogCategory} category={catalogCategory} />
</motion.div>
)}
</AnimatePresence>
@@ -91,6 +95,7 @@ export function ActionMenu({ className }: { className?: string }) {
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<div className="mx-auto w-max">
<FurnishTools />
@@ -127,6 +132,7 @@ export function ActionMenu({ className }: { className?: string }) {
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<div className="w-max">
<StructureTools />
@@ -21,12 +21,6 @@ export function ItemCatalog({ category }: { category: CatalogCategory }) {
const [activePlacementTag, setActivePlacementTag] = useState<string | null>(null);
const [activeFunctionalTag, setActiveFunctionalTag] = useState<string | null>(null);
// Reset tag filters when category changes
useEffect(() => {
setActivePlacementTag(null);
setActiveFunctionalTag(null);
}, [category]);
const categoryItems = CATALOG_ITEMS.filter(
(item) => item.category === category,
);
@@ -38,15 +38,6 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
const scanInputRef = useRef<HTMLInputElement>(null)
const guideInputRef = useRef<HTMLInputElement>(null)
const level = nodes[levelId as AnyNodeId] as LevelNode | undefined
if (!level) return null
// Find all scan and guide children of this level
const references = Object.values(nodes).filter(
(node): node is ScanNode | GuideNode =>
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
)
const handleAddScan = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
@@ -100,6 +91,15 @@ export function ReferencesDialog({ levelId, open, onOpenChange }: ReferencesDial
[deleteNode],
)
const level = nodes[levelId as AnyNodeId] as LevelNode | undefined
if (!level) return null
// Find all scan and guide children of this level
const references = Object.values(nodes).filter(
(node): node is ScanNode | GuideNode =>
(node.type === 'scan' || node.type === 'guide') && node.parentId === levelId,
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">