reactjstypescriptreact-ref

Forwarded ref is empty


I have the following forwarded ref in React:

export type AccordionHeaderProps<T extends ElementType> = {
  children: React.ReactNode;
  asTrigger?: boolean;
  subTitle?: React.ReactNode;
  chevronProps?: AriaButtonOptions<T>;
} & Options;

export const AccordionHeader = forwardRef(
  (
    {
      children,
      asTrigger,
      subTitle,
      chevronProps,
      id,
      className,
      isVisible,
      borderRadius,
      isLoading,
      error,
      isDisabled
    }: AccordionHeaderProps<"div">,
    ref
  ) => {
    const [accordionItemState, setAccordionItemState] = useContext(AccordionItemContext);

    const innerRef = useRef(null);
    useImperativeHandle(ref, () => innerRef.current!, []);

    const { buttonProps } = useButton(chevronProps || {}, innerRef as React.RefObject<HTMLDivElement>);

    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 onClick = () => {
      if (!asTrigger) return;
      if (iconState > 0) return;
      setAccordionItemState({ isExpanded: !accordionItemState.isExpanded });
      if (accordionItemState.isExpanded) innerRef.current.removeAttribute("hidden");
    };

    return (
      <div
        {...buttonProps}
        role="button"
        id={id}
        onClick={() => onClick()}
        className={header({
          class: `${className} ${borderRadius ? `rounded-${borderRadius}` : ""}`,
          isDisabled,
          isVisible
        })}
      >
        <div>
          {children}
          <br />
          <span className="text-slate-400">{subTitle}</span>
        </div>
        {icon[iconState]}
      </div>
    );
  }
);

The part to pay attention to is innerRef. innerRef.current is null. I want it to be a DOMRef. However, when I console log it I get null for current. Even if a wrap it a useEffect I still get the same result just printed twice.

What I want to be able to with the ref is remove a property from an element and add it back dynamically. I still want to be able to forward and pass a ref to useButton. I would ideally like to accomplish this with full type safety (at least for the ref prop in the forward ref). How can I accomplish this.

I am using React 18. Client components, no framework. The useButton hook comes from React aria.


Solution

  • Make sure your ref is passed down to your render output (assuming it's not part of buttonProps). i.e.

    return (
      <div
         {...buttonProps}
         role="button"
         ref={innerRef}
         { /* etc */ }
      />
    );