This article is based on the following source: How to Pass Component as Prop in React and TypeScript and explores additional use cases for the
Render Slot
pattern inReact
.
Creating Reusable and Framework Agnostic Link Component
Navigation can be tricky, especially when using frameworks like Gatsby
or Next.js
. Internal navigation tied to presentation often requires a substantial amount of additional code. Some developers prefer to use class names and simply glue them together to achieve the desired result. However, if your presentation layer is divided into components, particularly highly reusable ones, the topic begins to get complicated. Now, why does this issue arise?
Let's examine the challenges associated with external/internal navigation in applications and attempt to resolve them!
The Problem
Imagine you have a Link
component that internally uses the <a>
HTML element. This component provides some basic styling:
import React from 'react';
import c from 'classnames';
interface ButtonLinkProps {
className?: string;
to: string;
title: string;
rel?: string;
children: React.ReactNode;
target?: string;
}
const ButtonLink = ({ className, ...props }: ButtonLinkProps) => {
return (
<a
className={c(
`text-md shrink-0 cursor-pointer dark:outline-white focus:outline dark:outline-2 outline-2.5 outline-black text-center px-3 py-2 [&>svg]:text-2xl rounded-md font-medium font-sans bg-gray-300 text-black hover:bg-gray-400/70 dark:bg-slate-800 dark:hover:bg-slate-800/70 dark:text-white`,
className,
)}
{...props}
/>
);
};
export { ButtonLink };
What if you want to use NextLink
or a navigation component from another framework? You'd likely introduce a flag like this:
import NextLink from 'next/link';
interface ButtonLinkProps {
isNextLink?: boolean;
}
const ButtonLink = ({ className, isNextLink, ...props }: ButtonLinkProps) => {
// @@@ ⚠️ NextLink is being used, but we've coupled the presentation component with the Next framework... @@@
if (isNextLink) {
<NextLink
className={c(
`text-md shrink-0 cursor-pointer dark:outline-white focus:outline dark:outline-2 outline-2.5 outline-black text-center px-3 py-2 [&>svg]:text-2xl rounded-md font-medium font-sans bg-gray-300 text-black hover:bg-gray-400/70 dark:bg-slate-800 dark:hover:bg-slate-800/70 dark:text-white`,
className,
)}
{...props}
/>;
}
return (
<a
className={c(
`text-md shrink-0 cursor-pointer dark:outline-white focus:outline dark:outline-2 outline-2.5 outline-black text-center px-3 py-2 [&>svg]:text-2xl rounded-md font-medium font-sans bg-gray-300 text-black hover:bg-gray-400/70 dark:bg-slate-800 dark:hover:bg-slate-800/70 dark:text-white`,
className,
)}
{...props}
/>
);
};
It's problematic, indeed. First, we've coupled a framework-specific component with a presentation component. Consider what happens if you decide to migrate to Gatsby
. This would violate the Open/Closed principle from SOLID principles. You would need to manually change the implementation to fit a new contract. The flag isNextLink
is framework-specific, and in a new setting, it might become isGatsbyLink
or something similar, necessitating significant alterations.
The biggest problem is if your Design System is extracted as a standalone package. With the current approach, you would require every developer to install the Next.js
package just to use links, which wouldn't even be functional if they weren't using the Next.js
framework.
As you can see, if it were just a matter of style, it wouldn’t be a big deal. You could always create a shared class
and use it in two components: one for the design system and the other for application or framework-specific functionality. However, this approach would make your components less encapsulated and would require the style of this component to be attached at the start of the consumer application.
@import link.css
Let's address these issues using the simple React Render Slot Pattern
.
Making Link Trully Generic
We'll pass an optional function to our component's properties. By default, this function will implement a return of a native HTML <a>
element.
interface ButtonLinkProps {
className?: string;
to: string;
title: string;
rel?: string;
children: React.ReactNode;
target?: string;
// React Render prop.
component?: (props: Omit<ButtonLinkProps, 'component'>) => React.ReactNode;
}
This establishes a contract between the consumer (the parent component) and the Link
component. Now, let's proceed with the implementation.
const ButtonLink = ({
className,
// The default render prop implementation returns native <a> element.
component: Component = ({ to, ...props }) => <a href={to} {...props} />,
...props
}: ButtonLinkProps) => {
return (
// The component is rendered.
<Component
className={c(
`text-md shrink-0 cursor-pointer dark:outline-white focus:outline dark:outline-2 outline-2.5 outline-black text-center px-3 py-2 [&>svg]:text-2xl rounded-md font-medium font-sans bg-gray-300 text-black hover:bg-gray-400/70 dark:bg-slate-800 dark:hover:bg-slate-800/70 dark:text-white`,
className,
)}
{...props}
/>
);
};
export { ButtonLink };
This approach is great because it allows you to handle both external and internal navigation in the parent component, without needing to incorporate any framework-specific code into the purely presentation-focused components.
// Native HTML an element
<ButtonLink
to={meta.discordUrl}
target="_blank"
title={`${meta.company} Discord Channel`}
rel="noopener noreferrer"
>
Discord Channel
</ButtonLink>
// NextJS link component
import Link from "next/link";
<ButtonLink
to={meta.routes.docs.browse}
title="Navigate to education zone"
component={(props) => (
<Link activeClassName="active-button-link" {...props} />
)}
>
Education Zone
</ButtonLink>
The Demo
This solution is used on the site you're currently viewing! Check out the GIF below: one of the links takes you outside of the page - external navigation - while the second uses the GatsbyLink component for internal navigation in this particular case. Both share the same UI and appearance without code duplication.
Use Case in Real App
Summary
We've created a framework-agnostic Link
component. The best part is that with React
, the possibilities are virtually limitless. You can pass anything you want to your components, making them truly generic and adaptable to any situation.