reactjsnext.jsframer-motionreact-ref

how to fix function component cannot be given refs while using framer motion useAnimate hook?


on the left side close icons is animated when clicked after that the dropdown nav comein however i get error i.e on the left side

Navbar component code

import Humburger from "./ui/Humburger";
const navlins = ["HOME", "PAGES", "PRODUCTS", "ARTICLES", "CONTACT"];
const Navbar = () => {
  const [isNavbarActive, setIsNavbarActive] = useState(false);
  const toogleNavbar = () => setIsNavbarActive(!isNavbarActive);
  return (
    <>
      <div className="relative bg-white h-[4.5rem] z-20">
        <div className="w-full h-full flex justify-between items-center px-4">
          <Image
            width={120}
            height={70}
            src="https://cdn.prod.website-files.com/65478d390c8996a757a4faaa/65c4aacccb9a41d80d0d66f7_Stride-logo-dark.svg"
            alt="nav-logo"
          />
          <div className="hidden">
            <ul className="flex w-full border border-green-500">
              <li>Home</li>
              <li>Pages</li>
              <li>Product</li>
              <li>Article</li>
              <li>contact</li>
            </ul>
          </div>

          <div className="flex items-center gap-4">
            <div className="relative h-10 w-10 p-2">
              <Image
                className="z-0 inset-0"
                src="https://cdn.prod.website-files.com/65478d390c8996a757a4faaa/659e5cb3e032b694713f63c0_Add%20to%20basket.svg"
                width={20}
                height={20}
                alt="cart-icon"
              />
              <div className="z-20 absolute shadow-xl top-1 -right-[2px] bg-gray-200 text-black rounded-full text-[0.6em] h-[1.1rem] w-[1.1rem] grid place-content-center font-bold">
                5
              </div>
            </div>

            <button className="border flex items-center justify-center border-slate-400 hover:focus-within:none bg-black text-white rounded-full px-4 py-1.5 hover:bg-white transition-all ease-in hover:text-black">
              Login
            </button>

            <Humburger onClick={() => setIsNavbarActive(!isNavbarActive)} />
          </div>
        </div>
      </div>

      <AnimatePresence mode="popLayout">
        {isNavbarActive && <NavbarDropdown />}
      </AnimatePresence>
    </>
  );
};

export default Navbar;

const NavbarDropdown = () => {
  const dropdownVariants = {
    initial: {
      opacity: 0,
      height: 0,
    },
    enter: {
      opacity: 1,
      height: "14rem",
      transition: {
        ease: [0.83, 0, 0.17, 1],
        duration: 0.5,
        staggerChildren: 0.1,
        staggerDirection: 1,
        delayChildren: 0.2,
        // Stagger the li children after the dropdown opens
      },
    },
    exit: {
      height: 0,
      transition: {
        ease: [0.83, 0, 0.17, 1],
        staggerChildren: 0.1, // Stagger the li children on exit as well
        staggerDirection: -1,
        delay: 0.3,
        // Reverse the direction (exit from bottom to top)
        // Delay before shrinking the dropdown after the li items
      },
    },
  };

  const liVariants = {
    initial: {
      opacity: 0,
      x: -50,
    },

    enter: {
      opacity: 1,
      x: 0,
      transition: {
        ease: [0.22, 1, 0.36, 1],
        duration: 0.5,
      },
    },

    exit: {
      opacity: 0,
      x: -50,
      transition: {
        ease: [0.22, 1, 0.36, 1],
      },
    },
  };

  return (
    <motion.div
      variants={dropdownVariants}
      initial="initial"
      animate="enter"
      exit="exit"
      className="absolute origin-top top-[4.6rem] w-full bg-white drop-shadow-lg"
    >
      <motion.ul className="space-y-5 px-3">
        {navlins.map((navlin, index) => (
          <motion.li
            custom={index}
            key={index}
            variants={liVariants} // Apply variants to each li item
            className="text-gray-600 text-[1em] font-normal flex items-center gap-5"
          >
            {navlin}
            {index === 0 || index === 1 ? (
              <IoMdArrowDown size={24} color="gray" />
            ) : (
              <></>
            )}
          </motion.li>
        ))}
      </motion.ul>
    </motion.div>
  );
};

Humburger Component code

"use client";
import { useAnimate } from "framer-motion";
import React, { forwardRef, useState } from "react";

const Humburger = forwardRef<HTMLDivElement, { onClick: () => void }>(
  ({ onClick }, forwardedRef) => {
    const [scope, animate] = useAnimate();
    const [isHumburgOpen, setIsHumburgOpen] = useState(false);
    const [isAnimating, setIsAnimating] = useState(false);

    const animateHumburger = async () => {
      if (isAnimating) return null;

      setIsAnimating(!isAnimating);
      onClick();
      if (!isHumburgOpen) {
        animate(
          "#top",
          {
            y: "0.45rem",
          },
          {
            ease: [0.36, 0, 0.66, -0.56],
            duration: 0.5,
          }
        );
        await animate(
          "#bottom",
          {
            y: `-${0.45}rem`,
            opacity: 0,
          },
          {
            ease: [0.36, 0, 0.66, -0.56],
            duration: 0.5,
          }
        );
        animate(
          "#top",
          {
            rotate: "136deg",
          },
          {
            ease: [0.22, 1, 0.36, 1],
            duration: 0.5,
          }
        );
        animate(
          "#middle",
          {
            rotate: "43deg",
          },
          {
            ease: [0.22, 1, 0.36, 1],
            duration: 0.5,
          }
        );
        setIsHumburgOpen(!isHumburgOpen);
      } else {
        animate(
          "#top",
          {
            rotate: "0deg",
          },
          {
            ease: [0.36, 0, 0.66, -0.56],
            duration: 0.5,
          }
        );
        await animate(
          "#middle",
          {
            rotate: "0deg",
          },
          {
            ease: [0.36, 0, 0.66, -0.56],

            duration: 0.5,
          }
        );
        animate(
          "#top",
          {
            y: "0rem",
          },
          {
            ease: [0.22, 1, 0.36, 1],
            duration: 0.5,
          }
        );
        await animate(
          "#bottom",
          {
            y: 0,
            opacity: 1,
          },
          {
            ease: [0.22, 1, 0.36, 1],
            duration: 0.5,
          }
        );

        setIsHumburgOpen(!isHumburgOpen);
      }
      setIsAnimating(false);
    };
    return (
      <div ref={scope}>
        <a
          onClick={animateHumburger}
          className="hover:focus-within:outline-none hover:focus:outline-none flex flex-col items-end justify-center h-24 gap-1 mt-50 cursor-pointer"
        >
          <span
            id="top"
            className="origin-center h-[3px] w-6 bg-black  block rounded-full"
          ></span>
          <span
            id="middle"
            className="h-[3px] w-6  bg-black   rounded-full"
          ></span>
          <span
            id="bottom"
            className="h-[3px] w-4 bg-black  rounded-full"
          ></span>
        </a>
      </div>
    );
  }
);

export default Humburger;

the function animateHumburger is doing simply animating three vertical bar to a cross. am using framermotion to animate and am using useAnimate hook where i have to pass scope to the main container and id to sub element through which i will use in useanimatehook to animate them. after this humburger component is finished i have used it in navbar component but i get error .however the ref is being used in humburger component itself why i need to pass the useRef hook as suggested by chatgpt though it also didnot slove the problem . with function component cannot be given refs what does it actually mean ?? even in react doc they have used in functional component or am i getting it wrong. please correct me and help me to solve the issue ??

this image is from react official website where they its clearly show using use Ref in functional component or am i getting it wrong please correct me..

Also give me solution to the problem how to fix this error


Solution

  • The problem is that NavbarDropdown is not wrapped in forwardRef.

    The docs state:

    Custom component note: When using popLayout mode, any immediate child of AnimatePresence that's a custom component must be wrapped in React's forwardRef function, forwarding the provided ref to the DOM node you wish to pop out of the layout.

    You are using a custom component (NavbarDropdown) as the immediate child of AnimatePresence and you are also using popLayout mode:

    <AnimatePresence mode="popLayout">
        {isNavbarActive && <NavbarDropdown />}
    </AnimatePresence>
    

    So this constraint applies to you.

    To fix, wrap NavbarDropdown with forwardRef and apply that ref on the element you want to "pop". I have assumed here its the first element within NavbarDropdown.

    const NavbarDropdown = forwardRef<HTMLDivElement, {}>((props, ref) => {
      const dropdownVariants = {
        initial: {
          opacity: 0,
          height: 0,
        },
        enter: {
          opacity: 1,
          height: "14rem",
          transition: {
            ease: [0.83, 0, 0.17, 1],
            duration: 0.5,
            staggerChildren: 0.1,
            staggerDirection: 1,
            delayChildren: 0.2,
            // Stagger the li children after the dropdown opens
          },
        },
        exit: {
          height: 0,
          transition: {
            ease: [0.83, 0, 0.17, 1],
            staggerChildren: 0.1, // Stagger the li children on exit as well
            staggerDirection: -1,
            delay: 0.3,
            // Reverse the direction (exit from bottom to top)
            // Delay before shrinking the dropdown after the li items
          },
        },
      };
    
      const liVariants = {
        initial: {
          opacity: 0,
          x: -50,
        },
    
        enter: {
          opacity: 1,
          x: 0,
          transition: {
            ease: [0.22, 1, 0.36, 1],
            duration: 0.5,
          },
        },
    
        exit: {
          opacity: 0,
          x: -50,
          transition: {
            ease: [0.22, 1, 0.36, 1],
          },
        },
      };
    
      return (
        <motion.div
          ref={ref}
          variants={dropdownVariants}
          initial="initial"
          animate="enter"
          exit="exit"
          className="absolute origin-top top-[4.6rem] w-full bg-white drop-shadow-lg"
        >
          <motion.ul className="space-y-5 px-3">
            {navlins.map((navlin, index) => (
              <motion.li
                custom={index}
                key={index}
                variants={liVariants} // Apply variants to each li item
                className="text-gray-600 text-[1em] font-normal flex items-center gap-5"
              >
                {navlin}
                {index === 0 || index === 1 ? (
                  <IoMdArrowDown size={24} color="gray" />
                ) : (
                  <></>
                )}
              </motion.li>
            ))}
          </motion.ul>
        </motion.div>
      );
    });