The Decorator pattern (also known as Wrapper) is a structural design pattern that lets you attach new behaviors to objects or components by placing these objects inside special wrapper objects containing the behaviors.
1. Purpose and Problem
The Problem
Imagine you have a standard interface or basic component in your application (for instance, an HTML <a> tag for rendering hyperlinks).
As the project grows, you discover advanced requirements:
- Some links refer to technical glossary terms and should display a floating modal/tooltip instead of navigating away.
- External links must open in a new tab (
target="_blank" rel="noopener noreferrer"). - Certain internal links require interactive click tracking analytics.
If you tried to modify the base Markdown renderer directly or alter the DOM imperatively, you would break the Single Responsibility Principle (SRP) and couple rendering logic with business logic.
2. The Decorator Solution
Instead of altering the original component or creating multiple rigid subclasses, we wrap the base element inside a smart decorator.
The decorator object:
- Maintains the exact same interface or props as the wrapped element (
href,children,className, etc.). - Intercepts the call to evaluate dynamic rules.
- Adds the extra behavior (rendering a floating window or additional attributes) or delegates execution to the original element.
3. Real Example: MDX Link Interceptor (MdxLink)
In our Next.js and MDXRemote article architecture, we apply this pattern to dynamically decorate <a> tags:
// src/components/shared/ui/MdxLink.jsx
import GlossaryHover from './GlossaryHover';
export default function MdxLink({ href, children, ...props }) {
// 1. Decorator logic: Intercept glossary links
if (href && href.includes('/glossary/')) {
const term = href.split('/glossary/')[1]?.replace(/\/$/, '');
if (term) {
// Returns the enriched variant with floating tooltip
return <GlossaryHover term={term}>{children}</GlossaryHover>;
}
}
// 2. Default base behavior
return (
<a href={href} {...props}>
{children}
</a>
);
}Injection into Component Registry
Instead of polluting the PostArticle.jsx component with hyperlink conditionals, we inject the decorator directly into the component registry recognized by MDXRemote:
// src/components/shared/post/PostArticle.jsx
import MdxLink from '../../shared/ui/MdxLink';
const components = {
pre: Pre,
a: MdxLink, // Decorate <a> tag globally inside MDX
};
export default function PostArticle({ prefix, source }) {
return (
<article className={`${prefix}-article-content`}>
<MDXRemote source={source} components={components} />
</article>
);
}4. Advantages of this Approach
- Open/Closed Principle: You can add support for PDF downloads or click tracking inside
MdxLinkwithout touching your.mdxfiles or thePostArticlecomponent. - Author Transparency: Authors continue writing standard Markdown syntax
[Text](/path)without worrying about special components. - Complete Decoupling: Page layout and the Markdown processor remain clean and focused on their sole responsibilities.