I’ve been trying to reuse the CustomLink component, because of its animations. I need to use it once also as “NavLink” component. However now I feel like the CustomLink is accepting too many props.
Is it the only way to reuse it? I was also thinking of maybe storing the CSS styles in a variable that can be shared between different components and storing the animations in the ‘useCustomAnimation’ hook. However, i’d still have to pass the manually in each of them.
CustomLink.tsx
import { Link, NavLink } from "react-router-dom";
import styled, { css } from "styled-components";
import { useAnimate } from "framer-motion";
import { MutableRefObject } from "react";
import mergeRefs from "merge-refs";
type CustomLinkType = "regular" | "withUnderline";
type CustomLinkRenderAsType = "navlink" | "link";
const StyledCustomLink = styled(Link).attrs<{ $as: CustomLinkRenderAsType }>(
({ $as }) => ({
as: $as === "navlink" ? NavLink : Link,
})
)<{ $type: CustomLinkType }>`
${({ $type }) =>
($type === "regular" || $type === "withUnderline") &&
css`
text-decoration: none;
color: unset;
width: fit-content;
height: fit-content;
display: flex;
align-items: center;
justify-content: center;
`}
${({ $type }) =>
$type === "withUnderline" &&
css`
position: relative;
.link-underline {
position: absolute;
left: 0;
top: 100%;
width: 0;
height: 1px;
background-color: var(--color-dark-grey);
}
`};
${({ $as }) =>
$as === "navlink" &&
css`
padding: 2rem;
`};
`;
type CustomLinkProps = {
to: string;
$type?: CustomLinkType;
$as?: CustomLinkRenderAsType;
$ref?: MutableRefObject<HTMLAnchorElement | null> | null;
onHoverInCallback?: () => void;
onHoverOutCallback?: () => void;
};
export function CustomLink({
children,
to,
$as = "link",
$type = "regular",
$ref = null,
onHoverInCallback,
onHoverOutCallback,
}: CustomLinkProps & HasChildren) {
const [scope, animate] = useAnimate();
function customLinkAnimation(direction: "in" | "out") {
animate(
scope.current,
{
color:
direction === "in" ? "var(--color-dark-grey)" : "var(--color-black)",
},
{ duration: 0.2 }
);
if ($type === "withUnderline") {
animate(
".link-underline",
{ width: direction === "in" ? "100%" : 0 },
{ duration: 0.2 }
);
}
}
function onHoverIn() {
customLinkAnimation("in");
onHoverInCallback?.();
}
function onHoverOut() {
customLinkAnimation("out");
onHoverOutCallback?.();
}
return (
<StyledCustomLink
onMouseOver={onHoverIn}
onMouseLeave={onHoverOut}
to={to}
$as={$as}
ref={mergeRefs(scope, $ref)}
$type={$type}
>
{children}
{$type === "withUnderline" && <span className="link-underline"></span>}
</StyledCustomLink>
);
}
NavigationDropdown.tsx
import styled from "styled-components";
import { CustomLink } from "../CustomLink";
import { AnimatePresence, motion } from "framer-motion";
import { createPortal } from "react-dom";
import { useEffect, useRef, useState } from "react";
const NavigationDropdownContent = styled(motion.div)<{
$position: DOMRect | null;
}>`
width: 100%;
background-color: red;
position: fixed;
left: 0;
top: ${({ $position }) =>
$position && $position.top + $position.height + "px"};
overflow: hidden;
`;
type NavigationDropdownProps = {
navigationLinksTo: string;
navigationLinkText: string;
};
export function NavigationDropdown({
children,
navigationLinksTo,
navigationLinkText,
}: NavigationDropdownProps & HasChildren) {
const [isDropdownCollapsed, setIsDropdownCollapsed] =
useState<boolean>(false);
const navigationLink = useRef<HTMLAnchorElement | null>(null);
const [navLinkPosition, setNavLinkPosition] = useState<DOMRect | null>(null);
useEffect(() => {
if (navigationLink.current && isDropdownCollapsed) {
const navigationLinkLocation =
navigationLink.current.getBoundingClientRect();
setNavLinkPosition(navigationLinkLocation);
}
}, [isDropdownCollapsed]);
function onHoverIn() {
setIsDropdownCollapsed(true);
}
function onHoverOut() {
setIsDropdownCollapsed(false);
}
return (
<CustomLink
onHoverInCallback={onHoverIn}
onHoverOutCallback={onHoverOut}
$type="withUnderline"
$as="navlink"
to={navigationLinksTo}
$ref={navigationLink}
>
{navigationLinkText}
{createPortal(
<AnimatePresence>
{isDropdownCollapsed && (
<NavigationDropdownContent
exit={{ height: 0 }}
animate={{ height: "100%" }}
$position={navLinkPosition}
>
{children}
</NavigationDropdownContent>
)}
</AnimatePresence>,
document.querySelector("#root")!
)}
</CustomLink>
);
}
Юра Д is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.