You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>
  );
});

关键修复说明:

  1. 绑定Ref到DOM元素
    原代码中innerRef未绑定到任何返回的DOM节点,这是current始终为null的根本原因。现在在根<div>上添加ref={innerRef},让React将DOM节点赋值给innerRef.current。

  2. 类型安全优化

    • 修正forwardRef的泛型定义,让组件的Ref类型与传入的ElementType自动匹配,默认使用div对应的HTMLDivElement。
    • 去掉不必要的useImperativeHandle:仅需转发原生DOM Ref时,直接关联外部Ref与内部Ref即可,无需自定义暴露方法(若需暴露自定义逻辑再使用useImperativeHandle)。
  3. React Aria useButton 适配
    将innerRef直接传递给useButton,确保React Aria能正确访问DOM元素,处理无障碍属性与交互逻辑。

  4. DOM属性操作修正
    使用可选链?.避免空值报错,同时修正状态切换时的属性操作逻辑(原逻辑判断反了)。

  5. 事件处理简化
    将嵌套的onClick={() => onClick()}改为直接传递handleClick,避免不必要的函数嵌套。

内容的提问来源于stack exchange,提问作者Ethan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 18:55:13