Open-source software provides a great learning opportunity, and in this series, I’ll explore solutions to specific problems I’ve encountered while working with code and documentation from open-source projects.
Today, let’s take a look at the linkComponent property in the Braid Design System by SEEK.
The challenge of handling links in different applications
When building a component library, it needs to be flexible enough to work across different applications, each of which might have its own way of handling links.
- Link in react-router
- Next.js Link component
- an anchor tag
It wouldn’t be scalable or efficient to manage multiple Link components for every possible routing solution. Instead, SEEK came up with an elegant solution.
Seeks solution using React context
Instead of creating different Link components for each use case, SEEK's team came up with an elegant solution: they added a linkComponent property on their BraidProvider. This approach allows developers to specify a single Link component that works seamlessly across different routing implementations. Whether it’s React Router, Next.js, or an anchor tag, the linkComponent ensures that the appropriate link behavior is consistently applied throughout the application, without duplicating the code or logic for each specific case.
import React from "react"
import { Link as ReactRouterLink } from "react-router-dom"
import { BraidProvider, makeLinkComponent } from "braid-design-system"
// internal links via ReactRouterLink
// external via anchor tag
const CustomLink = makeLinkComponent(({ href, ...restProps }, ref) =>
href[0] === "/" ? (
<ReactRouterLink ref={ref} to={href} {...restProps} />
) : (
<a ref={ref} href={href} {...restProps} />
)
)
export const App = () => (
<BraidProvider linkComponent={CustomLink}>
// ...
</BraidProvider>
)
Let's take a look at the implementation, which is fairly straightforward. First, they set up context and provided a sensible default (anchor tag)
export interface LinkComponentProps
extends AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
}
export type LinkComponent =
| ReturnType<typeof makeLinkComponent>
| ComponentType<LinkComponentProps>;
const DefaultLinkComponent = (props) => (
<a {...props} />
)
const LinkComponentContext = createContext<LinkComponent>(DefaultLinkComponent);
They created and exported a useLinkComponent hook that returns the value in context.
export const useLinkComponent = (ref: Ref<HTMLAnchorElement>) => {
const linkComponent = useContext(LinkComponentContext);
// assert + ref check removed here
return linkComponent;
};
Lastly, the BraidProvider receives a linkComponent and passes it down (if set) to the context.
export interface BraidProviderProps {
linkComponent?: LinkComponent;
children: ReactNode;
}
export const BraidProvider = ({
linkComponent,
children,
}: BraidProviderProps) => {
const linkComponentFromContext = useContext(LinkComponentContext);
// other Providers + props being passed down
return (
<LinkComponentContext.Provider
value={linkComponent || linkComponentFromContext}
>
{children}
</LinkComponentContext.Provider>
);
};
Whenever a component needs to retrieve the configured Link component, it uses the useLinkComponent hook.
Link / source
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
({ href, className, ...restProps }, ref) => {
const LinkComponent = useLinkComponent(ref);
return (
<LinkComponent
ref={ref}
href={href}
className={clsx(atoms({ reset: 'a' }), className)}
{...restProps}
/>
);
},
);
ButtonLink / source
<ButtonContainer bleed={bleed} variant={variant}>
<Box
component={LinkComponent}
ref={ref}
{...restProps}
{...buildDataAttributes({ data, validateRestProps: false })}
{...useButtonStyles({
variant,
tone,
size,
bleed: bleed || bleedY,
loading,
})}
>
<ButtonOverlays variant={variant} tone={tone} />
<ButtonText
variant={variant}
tone={tone}
size={size}
loading={loading}
icon={icon}
iconPosition={iconPosition}
bleed={bleed}
>
{children}
</ButtonText>
</Box>
</ButtonContainer>
I've implemented this pattern several times in custom pattern libraries. Credit where credit is due.
Member discussion