react
patterns
render-slot
typescript
tailwind

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 in React.

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.

The Generic Links Demo 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.