Back to articlesTutorial
The Proxy Component in React
A simple pattern to shield your codebase from change: centralise an implementation instead of scattering it across every file.
#react#patterns#components
Mike Codeur
August 01, 20265 minTutorial
Behind the intimidating name hides a very simple idea — and probably the React pattern that saves you the most time in the long run: never let an implementation spread across your codebase.
A Proxy Component is a component whose only job is to wrap another one. It barely does anything. That is exactly why it is useful.
All fine — until the day the button implementation has to change: switch to the company component library, add a class, wire up analytics tracking:
Now you are off on a find-and-replace across the whole project. With five occurrences, fine. With a hundred and fifty spread over forty files, that is a lost day — and a good chance of missing three of them.
Then use it everywhere:
At first glance you have only added indirection. In practice you have created a single point of change. The day the implementation evolves, exactly one file moves:
Nothing else changes. That is the whole point of the pattern.
Same thing when wrapping an existing component rather than an HTML element:
The first form is almost always the right one: it provides defaults without locking anything down. Keep the second for things that must never vary, such as a
Six lines, and a security hole (a missing
The problem
Take an app with "Like" buttons all over the place:function Header() {
return (
<div>
<h1>Welcome</h1>
<button>Like</button>
</div>
)
}
function Content() {
return (
<div>
<h2>Articles</h2>
<span>Article 1</span>
<button>Like</button>
<span>Article 2</span>
<button>Like</button>
</div>
)
}
function Footer() {
return (
<div>
<h3>Contact us</h3>
<button>Like</button>
</div>
)
}// before
<button>Like</button>
// after
<Button variant="ghost" size="sm" onClick={track}>Like</Button>The real cost is not the number of lines to change, it is the risk. Every
missed occurrence becomes a visual inconsistency nobody notices until it is in
production.
The fix
Create a component that does nothing but delegate:function LikeButton(props) {
return <button {...props}>Like</button>
}function Header() {
return (
<div>
<h1>Welcome</h1>
<LikeButton />
</div>
)
}function LikeButton(props) {
return (
<Button variant="ghost" size="sm" {...props}>
Like
</Button>
)
}The key detail: spreading props
The{...props} is not cosmetic — it is what makes the component genuinely reusable. Without it, your proxy becomes a wall: you cannot pass an onClick, a disabled or an aria-label without editing the wrapper every single time.
function LikeButton(props) {
return <button {...props}>Like</button>
}
// everything works, no change to the proxy
<LikeButton onClick={handleLike} disabled={isPending} aria-label="Like" />A proxy that does not forward its props is worse than no proxy at all:
attributes vanish without any error. You end up debugging the parent when
the bug lives in the wrapper.
In TypeScript
Type your props from the element you are wrapping and you inherit everything it accepts:import type {ComponentProps} from 'react'
type LikeButtonProps = ComponentProps<'button'>
export function LikeButton(props: LikeButtonProps) {
return <button {...props}>Like</button>
}import type {ComponentProps} from 'react'
import {Button} from '@/components/ui/button'
type LikeButtonProps = ComponentProps<typeof Button>
export function LikeButton(props: LikeButtonProps) {
return (
<Button variant="ghost" size="sm" {...props}>
Like
</Button>
)
}In React 19,
ref is a regular prop: it is part of ComponentProps and
travels through the spread. No more forwardRef needed to write a transparent
proxy.Prop order: who wins?
A detail that matters — where you place the spread decides who has the final say.// defaults — the caller can override them
<Button variant="ghost" {...props} />
// enforced — the caller cannot change them
<Button {...props} variant="ghost" />type="button" that prevents an accidental form submission.
Where it actually pays off
1
Wrapping a third-party library
Every button goes through your own component instead of the library's. The day
you switch UI libraries, you rewrite a handful of files, not an application.
2
Enforcing a product convention
Every external link must carry a blank target and the matching
rel? A proxy
guarantees it once and for all, instead of relying on everyone's attention
during code review.3
Adding cross-cutting behaviour
Analytics, logging, loading state: the proxy is the natural place to add
behaviour to every usage of a component at once.
4
Absorbing a migration
During an API change, the proxy translates the old interface into the new one.
The rest of the codebase keeps running while you migrate.
A concrete example: the external link
import type {ComponentProps} from 'react'
type ExternalLinkProps = ComponentProps<'a'>
export function ExternalLink(props: ExternalLinkProps) {
return <a target="_blank" rel="noopener noreferrer" {...props} />
}rel on a blank target) becomes structurally impossible in your app.
When NOT to use it
The pattern is simple, therefore tempting. Two guardrails:| Situation | Verdict |
|---|---|
| A component used in three or more places | Good candidate |
| A convention to guarantee (security, a11y) | Good candidate, even for two usages |
| A single usage with no reuse in sight | Pointless — indirection for nothing |
| A proxy wrapping a proxy wrapping a proxy… | Red flag: flatten it |
The right question is not "does this repeat?" but "what is going to
change?". You isolate what moves, not what looks alike.
Takeaway
A Proxy Component is three lines of code that turn a global change into a local one. It requires no library and no clever abstraction — just the discipline of never calling an implementation directly from forty different places. That is the real spirit of DRY: not "never write the same thing twice", but "have a single place to edit when a decision changes".Written by
MC
Mike Codeur
Author
Related Articles
DevelopmentIntroduction to Next.js 16
What actually changes when you build with Next.js 16 and React 19: async params, Server Components, Server Actions and Turbopack.
Aug 03, 20265 min
Read more
Design
Tailwind CSS v4: configuration moves into your CSS
No more tailwind.config.js. v4 moves the whole configuration into your stylesheet, with @theme, @plugin and @custom-variant.
Aug 02, 20264 min
Read more