React中Forwarded Ref为空:如何获取DOMRef并实现类型安全?
问题解决:修复AccordionHeader组件的Ref绑定与类型安全问题
先直接上修复后的完整代码,解决innerRef.current为null的核心问题,同时保证类型安全并兼容React Aria的useButton:
import { forwardRef, useContext, useRef } from 'react'; import { useButton, AriaButtonOptions } from '@react-aria/button'; import { ElementType } from 'react'; import { motion } from 'framer-motion'; import { ChevronLeft, AlertCircle } from 'lucide-react'; // 假设Options类型定义(请根据实际场景调整) type Options = { id?: string; className?: string; isVisible?: boolean; borderRadius?: 'sm' | 'md' | 'lg'; isLoading?: boolean; error?: boolean; isDisabled?: boolean; }; export type AccordionHeaderProps<T extends ElementType = 'div'> = { children: React.ReactNode; asTrigger?: boolean; subTitle?: React.ReactNode; chevronProps?: AriaButtonOptions<T>; } & Options; export const AccordionHeader = forwardRef< T extends ElementType ? React.ElementRef<T> : HTMLDivElement, AccordionHeaderProps >((props, ref) => { const { children, asTrigger, subTitle, chevronProps, id, className, isVisible, borderRadius, isLoading, error, isDisabled } = props; const [accordionItemState, setAccordionItemState] = useContext(AccordionItemContext); // 创建内部Ref,用于绑定DOM元素并关联外部传入的Ref const innerRef = useRef<React.ElementRef<typeof props.asElement || 'div'>>(null); // 关联外部Ref与内部Ref if (ref) { if (typeof ref === 'function') { ref(innerRef.current); } else { ref.current = innerRef.current; } } // 传递innerRef给useButton,确保React Aria能访问DOM元素 const { buttonProps } = useButton(chevronProps || {}, innerRef); const icon = [ <motion.div animate={{ rotate: accordionItemState.isExpanded ? '-90deg' : '0deg' }} whileHover={{ background: '#f1f5f9' }} whileTap={{ scale: 0.9 }} initial={{ background: 'transparent' }} transition={{ ease: 'easeInOut' }} className="ml-auto rounded-sm" > <ChevronLeft width={25} /> </motion.div>, <div className="ml-auto"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" width="25" height="25" style={{ shapeRendering: 'auto', display: 'block', background: 'transparent' }} > <g> <circle stroke-dasharray="122.52211349000194 42.840704496667314" r="26" stroke-width="5" stroke="#000000" fill="none" cy="50" cx="50" > <animateTransform keyTimes="0;1" values="0 50 50;360 50 50" dur="1s" repeatCount="indefinite" type="rotate" attributeName="transform" ></animateTransform> </circle> <g></g> </g> </svg> </div>, <div className="ml-auto"> <AlertCircle width={25} height={25} /> </div> ]; let iconState = 0; if (isLoading) iconState = 1; else if (error) iconState = 2; const handleClick = () => { if (!asTrigger || iconState > 0) return; const newExpandedState = !accordionItemState.isExpanded; setAccordionItemState({ isExpanded: newExpandedState }); // 操作DOM属性,使用可选链避免空值报错 if (newExpandedState) { innerRef.current?.removeAttribute('hidden'); } else { innerRef.current?.setAttribute('hidden', 'true'); } }; return ( <div {...buttonProps} ref={innerRef} // 核心:将innerRef绑定到当前DOM元素,解决current为null的问题 role="button" id={id} onClick={handleClick} className={header({ class: `${className} ${borderRadius ? `rounded-${borderRadius}` : ''}`, isDisabled, isVisible })} > <div> {children} <br /> <span className="text-slate-400">{subTitle}</span> </div> {icon[iconState]} </div> ); });
关键修复说明:
绑定Ref到DOM元素
原代码中innerRef未绑定到任何返回的DOM节点,这是current始终为null的根本原因。现在在根<div>上添加ref={innerRef},让React将DOM节点赋值给innerRef.current。类型安全优化
- 修正
forwardRef的泛型定义,让组件的Ref类型与传入的ElementType自动匹配,默认使用div对应的HTMLDivElement。 - 去掉不必要的
useImperativeHandle:仅需转发原生DOM Ref时,直接关联外部Ref与内部Ref即可,无需自定义暴露方法(若需暴露自定义逻辑再使用useImperativeHandle)。
- 修正
React Aria useButton 适配
将innerRef直接传递给useButton,确保React Aria能正确访问DOM元素,处理无障碍属性与交互逻辑。DOM属性操作修正
使用可选链?.避免空值报错,同时修正状态切换时的属性操作逻辑(原逻辑判断反了)。事件处理简化
将嵌套的onClick={() => onClick()}改为直接传递handleClick,避免不必要的函数嵌套。
内容的提问来源于stack exchange,提问作者Ethan
相关产品推荐
相关产品推荐

