Stop Polluting Your React Components with Analytics Code
After some time, I found this problem again: adding tracking for analytics. The task seems straightforward — catch user interactions and send the data somewhere. Click a button? Track it. View a page? Track it. Easy.
It looks simple, but it’s not. If you do it wrong, your component gets polluted (contaminado) with shit it shouldn’t care about.
How It Starts (The Innocent Beginning)
Let’s say you need to track when users click a purchase button. The most direct approach:
function ContainerComponent() {
function handleClick() {
// Your actual business logic
processPurchase()
// And boom, analytics right here
window.dataLayer.push({
event: 'purchase_click',
productId: '123'
})
}
return (
<div>
<button onClick={handleClick}>Buy</button>
</div>
)
}This works, but feels ugly having window.dataLayer.push right there in your business logic.
The Problem Grows (And Your Component Gets Fucked)
Marketing wants view tracking. Product wants hover tracking. Your innocent component becomes an analytics management center:
function ContainerComponent() {
function handleClick() {
processPurchase() // Remember this? Your actual job?
window.dataLayer.push({
event: 'purchase_click',
productId: '123'
})
}
useEffect(() => {
// View tracking
window.dataLayer.push({
event: 'purchase_view',
productId: '123'
})
}, [])
return (
<div>
<button
onClick={handleClick}
onMouseEnter={() => {
// Hover tracking
window.dataLayer.push({
event: 'purchase_hover',
productId: '123'
})
}}
>
Buy
</button>
</div>
)
}Now your component is responsible for:
- Processing purchases (its actual job)
- Managing click tracking
- Managing view tracking
- Managing hover tracking
This is the problem I’m talking about. Your component is contaminated with responsibilities it shouldn’t have.
What Developers Try Next (Custom Hooks to “Clean” Things Up)
When developers notice this pollution, they think “let’s create custom hooks to organize this better”:
function useAnalytics(trackingOptions) {
const trackClick = () => {
window.dataLayer.push({
event: 'click',
...trackingOptions
})
}
const trackView = () => {
window.dataLayer.push({
event: 'view',
...trackingOptions
})
}
return { trackClick, trackView }
}
// But your component still has to orchestrate everything
function ContainerComponent() {
const { trackClick, trackView } = useAnalytics({
component: 'purchase-button',
productId: '123'
})
// Still polluted with tracking responsibilities
useEffect(() => {
trackView()
}, [])
function handleClick() {
processPurchase() // Your real job
trackClick() // Still your problem
}
return <button onClick={handleClick}>Buy</button>
}This organizes the code better, but doesn’t solve the real issue: your component still knows about analytics and has to manage it.
The Real Solution: Get Analytics Out of Your Component
What if your component never had to know about tracking at all? This is where Higher-Order Components shine.
Get Paul’s stories in your inbox
Join Medium for free to get updates from this writer.
Instead of putting tracking inside your component, you wrap your component with tracking capabilities:
function withOnClick(Component, config) {
return function({ meta, onClick, ...props }) {
const { onClickAction } = use(TaggingContext) ?? {}
function handleOnClick() {
// here we can consume a context -> the way how we inject dependencies in react to call the function/method that has the job to send this infromation to the respective analytics.
onClickAction?.(config, meta)
onClick?.()
}
return <Component {...props} onClick={handleOnClick} />
}
}Now you can wrap your components with tracking behavior:
const TrackedBuyButton = withOnClick(BuyButton, { action: 'buy-clicked' })The Beautiful Result
Your component stays completely clean and focused on its real job:
function BuyButton({ onClick }) {
return <button onClick={onClick}>Buy</button>
}
function ContainerComponent() {
function handlePurchase() {
// Only business logic here - no analytics shit
processPurchase()
}
return (
<div>
<TrackedBuyButton
onClick={handlePurchase}
meta={{ productId: '123', price: 99.99 }}
/>
</div>
)
}Look at that! Your ContainerComponent only cares about processing purchases. The BuyButton only cares about being a button. Analytics is handled externally through composition.
Composing Multiple Tracking Behaviors
You can also create different HOCs for different tracking needs and compose them together:
// Compose multiple tracking behaviors
const FullyTrackedButton = withOnView(
withOnClick(BuyButton, { action: 'buy-clicked' }),
{ action: 'buy-button-viewed' }
)
// Usage is the same - your component stays clean
function ContainerComponent() {
function handlePurchase() {
processPurchase()
}
return (
<div>
<FullyTrackedButton
onClick={handlePurchase}
meta={{ productId: '123', price: 99.99 }}
/>
</div>
)
}Same component, different tracking behaviors. Your component never knows or cares about analytics.
And that’s it — clean components that focus on their real job, with analytics handled externally.







