Before reading this: Please read Swizec Teller's original article:
This codebase is a response to that article. Understanding Swizec's argument is essential context for the counter-thesis presented here.
The problem with DRY (Don't Repeat Yourself) isn't the principle itself, but the approach. Instead of anticipating flexibility through configuration props, expose internal primitives for composition.
In his article, Swizec identifies a critical problem with premature abstraction. He describes how a generic button component evolves:
"You start with a simple button component. Then you need a blue one. Then a green one. Then one that's disabled sometimes. Then one that's only disabled when some other state is true. Then..."
The pattern he describes is familiar to anyone who's worked in a large React codebase: a component starts simple, accumulates props to handle edge cases, and eventually becomes a configuration nightmare:
// The anti-pattern Swizec warns against
<GenericButton
variant="primary"
size="large"
disabled={!isActive}
color="green"
onClick={handleClick}
showLoader={isLoading}
iconPosition="left"
// ... 15 more props
/>Swizec's conclusion:
DRY leads to bloated abstractions. Use YAGNI instead—don't abstract until patterns genuinely emerge.
(YAGNI: "You Aren't Gonna Need It")
Swizec is right about the symptom, but the diagnosis misses something crucial. The issue isn't DRY itself—it's configuration-based abstraction.
When you try to make a component flexible by adding props for every scenario, you're making a flawed assumption:
You can predict what flexibility points your consumers will need.
You can't.
This codebase demonstrates an alternative approach: don't predict flexibility—provide composability.
Let's make this concrete. Imagine you need buttons with different colors and behaviors.
// Attempt to predict all variations
const GenericButton = ({
variant,
color,
size,
disabled,
loading,
onClick,
children
}) => {
const styles = {
backgroundColor: color === 'primary' ? 'blue' : color === 'danger' ? 'red' : 'gray',
fontSize: size === 'large' ? '18px' : '14px',
opacity: disabled ? 0.5 : 1,
// ... more conditional styling
};
return (
<button
style={styles}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? 'Loading...' : children}
</button>
);
};Why this fails:
- Every new variation requires modifying the component
- Props proliferate as you discover edge cases
- The component becomes brittle—changing one case risks breaking others
- Consumers are constrained by your predictions
import { createElement, useCallback } from 'react';
// Expose internal primitives for composition
const GenericButton = ({ children, onClick, ...enhancement }) => {
const track = callback =>
useCallback(
(...params) => console.log(callback.toString()) || callback(...params),
[callback]
);
const style = {
border: "1px solid black",
fontSize: "14px",
padding: "10px",
cursor: "pointer"
};
const Button = props => (
<button
onClick={track(onClick)}
style={style}
{...props}
{...enhancement}
/>
);
// The key: expose pieces for composition
const pieces = { Button, track, style };
const isComponent = object => typeof object === "function";
return isComponent(children)
? createElement(children, pieces) // Pass pieces to function child
: <Button>{children}</Button>;
};Why this succeeds:
GenericButtonsolves one problem: a tracked, styled button primitive- Internal pieces (
Button,track,style) are explicitly exposed - Consumers compose what they need without
GenericButtonknowing about their use cases - No props proliferation—flexibility comes from composition, not configuration
Note: The key mechanism is using createElement to invoke the function child with pieces: createElement(children, pieces). This is what enables the render props pattern.
The critical insight is in how GenericButton handles children using the Render Props pattern:
const isComponent = object => typeof object === "function";
return isComponent(children)
? createElement(children, pieces) // Advanced: pass primitives for composition
: <Button>{children}</Button>; // Simple: just render a buttonThis dual consumption pattern enables:
<GenericButton onClick={() => alert('clicked')}>
Click Me
</GenericButton><GenericButton onClick={() => alert('clicked')}>
{({ Button, style, track }) => (
<Button style={{ ...style, background: 'green' }}>
Custom Button
</Button>
)}
</GenericButton>The consumer decides the complexity level. GenericButton doesn't predict it.
Let's trace how different components consume GenericButton with varying levels of composition.
Level 1: Direct Primitive Usage (ClickMe.js)
import { createElement } from 'react';
import GenericButton from './GenericButton';
const ClickMe = () => (
<GenericButton onClick={() => alert("closePage()")}>
{({ Button, style }) => (
<Button style={{ ...style, background: "blue" }}>
ClickMe
</Button>
)}
</GenericButton>
);
export default ClickMe;What's happening:
- Receives
ButtonandstylefromGenericButton - Spreads base styles and overrides
background - Reuses tracking and base button logic without reimplementation
Level 2: Layered Composition (Activable.js)
import { createElement } from 'react';
import GenericButton from './GenericButton';
const identify = component =>
Object.assign(component, { displayName: "Custom(GenericButton)" });
const Activable = ({ onClick, children, active }) => (
<GenericButton onClick={onClick} disabled={!active}>
{pieces => createElement(identify(children), pieces)}
</GenericButton>
);
export default Activable;What's happening:
- Wraps
GenericButtonto add activation state behavior - Acts as a composition middleware—receives pieces from
GenericButtonand forwards them - Uses
createElement(identify(children), pieces)to invoke the child function with pieces - Uses
identifyto set componentdisplayNamefor React DevTools - Doesn't know what children will do with pieces
- Single responsibility: map
activeprop todisabledstate
Level 3: Complex Composition with Side Effects (Input.js)
import { createElement, useState, useEffect } from 'react';
import GenericButton from './GenericButton';
const URL = "https://placekitten.com/300/300";
const createImage = src => {
const img = new Image();
img.src = src;
return img;
};
const Input = () => {
const [loading, setLoading] = useState(true);
const load = () => setLoading(false);
useEffect(() => {
createImage(URL).addEventListener("load", load, true);
}, []);
return (
<GenericButton>
{({ track, style }) =>
loading ? (
<p style={{ ...style, border: "none", cursor: "default" }}>
Loading image...
</p>
) : (
<input
type="image"
src={URL}
alt="Image as button"
onClick={track(() => alert("clickImage()"))}
onMouseOver={track(() => console.log("mouseOverImage()"))}
onMouseOut={track(() => console.log("mouseOutImage()"))}
style={{ ...style, padding: "0" }}
/>
)
}
</GenericButton>
);
};
export default Input;What's happening:
- Uses only
trackandstylepieces (ignoresButton) - Renders completely different elements (
<p>or<input>) - Demonstrates that pieces are à la carte—use what you need
GenericButtonnever anticipated this use case, yet it works perfectly- Uses React Hooks (
useState,useEffect,useCallback) for state management
Let's formalize what makes this pattern work.
// GenericButton's pieces contract
type ButtonPieces = {
Button: React.ComponentType<React.ButtonHTMLAttributes<HTMLButtonElement>>;
track: <T extends (...args: any[]) => any>(callback: T) => T;
style: React.CSSProperties;
};
// GenericButton accepts either:
// 1. ReactNode (simple usage)
// 2. Function receiving pieces (composition usage)
type GenericButtonProps = {
onClick: () => void;
children: React.ReactNode | ((pieces: ButtonPieces) => React.ReactNode);
} & React.HTMLAttributes<HTMLButtonElement>;Traditional configuration-based components force the component to control how consumers use it:
// Component controls the consumer
<GenericButton variant="primary" /> // Consumer limited to predefined variantsComposition-based components invert this (Inversion of Control):
// Consumer controls the composition
<GenericButton>
{({ Button, style }) => /* Consumer decides what to render */}
</GenericButton>This is true Dependency Inversion at the component level.
Let's revisit Swizec's concern: as requirements evolve, configuration-based components become unmaintainable.
Requirement: "We need a button that shows an image after loading."
// Now GenericButton needs to know about images and loading states
<GenericButton
variant="image-loader"
imageUrl="..."
showLoadingText={true}
loadingText="Loading image..."
onImageLoad={...}
/>Every new requirement modifies GenericButton. This is the footgun Swizec warns about:
"Every time you need a variation, you modify the shared component. It grows. It becomes complex. Eventually it's easier to duplicate than to use."
// GenericButton doesn't change at all
<GenericButton>
{({ track, style }) =>
loading ? (
<p style={style}>Loading...</p>
) : (
<input
type="image"
src={imageUrl}
onClick={track(onClick)}
/>
)
}
</GenericButton>The requirement is handled at the consumer level using exposed primitives. GenericButton remains untouched.
Let's directly address every requirement Swizec mentions. The key insight: GenericButton never changes—all variations are handled by consumers composing primitives.
Configuration approach (modifies GenericButton):
// Add color prop to GenericButton
<GenericButton color="blue">Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) => (
<Button style={{ ...style, background: "blue" }}>
Click Me
</Button>
)}
</GenericButton>See implementation: src/components/ClickMe.js
Configuration approach (adds to GenericButton):
// Now GenericButton needs to handle both blue and green
<GenericButton color="green">Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) => (
<Button style={{ ...style, background: "green" }}>
Click Me
</Button>
)}
</GenericButton>See implementation: src/components/Simple.js
Configuration approach (adds disabled prop):
<GenericButton color="blue" disabled={true}>Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) => (
<Button disabled style={{ ...style, background: "blue" }}>
Click Me
</Button>
)}
</GenericButton>Note: Through ...enhancement, any consumer can already pass disabled without GenericButton knowing.
Configuration approach (adds conditional logic):
// GenericButton now needs to understand your business logic
<GenericButton color="blue" disabled={!isActive}>Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick} disabled={!isActive}>
{({ Button, style }) => (
<Button style={{ ...style, background: "blue" }}>
Click Me
</Button>
)}
</GenericButton>See implementation: src/components/Activable.js (wraps GenericButton to add activation behavior)
Configuration approach (adds size variants):
// GenericButton now needs size mapping logic
<GenericButton color="blue" size="large">Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) => (
<Button style={{ ...style, background: "blue", fontSize: "18px", padding: "15px" }}>
Click Me
</Button>
)}
</GenericButton>Configuration approach (adds loading state):
// GenericButton now manages loading UI
<GenericButton color="blue" showLoader={isLoading}>Click Me</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) =>
isLoading ? (
<Button disabled style={{ ...style, background: "blue" }}>
Loading...
</Button>
) : (
<Button style={{ ...style, background: "blue" }}>
Click Me
</Button>
)
}
</GenericButton>See implementation: src/components/Input.js (demonstrates loading state with image)
Configuration approach (adds icon support):
// GenericButton now needs icon rendering logic
<GenericButton
color="blue"
icon={<SaveIcon />}
iconPosition="left"
>
Save
</GenericButton>Composition approach (GenericButton unchanged):
<GenericButton onClick={handleClick}>
{({ Button, style }) => (
<Button style={{ ...style, background: "blue" }}>
<SaveIcon /> Save
</Button>
)}
</GenericButton>Configuration approach (breaks down):
// GenericButton can't handle this—it's fundamentally about <button>
// You'd need a new component or hacky as="input" propComposition approach (GenericButton unchanged):
<GenericButton>
{({ track, style }) => (
<input
type="image"
src={imageUrl}
onClick={track(handleClick)}
style={{ ...style, padding: "0" }}
/>
)}
</GenericButton>See implementation: src/components/Input.js (uses <input type="image"> instead of <button>)
Notice what happened across all 8 requirements:
| Requirement | Configuration Approach | Composition Approach | GenericButton Modified? |
|---|---|---|---|
| Blue button | Added color prop |
Composed with style |
❌ No |
| Green button | Extended color prop |
Composed with style |
❌ No |
| Disabled | Added disabled prop |
Used ...enhancement |
❌ No |
| Conditional disabled | Added conditional logic | Consumer handles logic | ❌ No |
| Different sizes | Added size prop |
Composed with style |
❌ No |
| Loading state | Added showLoader prop |
Consumer handles state | ❌ No |
| Icons | Added icon, iconPosition |
Composed children | ❌ No |
| Different element | Can't handle / needs refactor | Composed with track, style |
❌ No |
Configuration approach: 7 prop additions, 1 impossible case, GenericButton grows with every requirement.
Composition approach: 0 modifications to GenericButton, all cases handled, including the "impossible" one.
const pieces = { Button, track, style };
const isComponent = object => typeof object === "function";
return isComponent(children)
? createElement(children, pieces) // Invoke function child with pieces
: <Button>{children}</Button>;Why: Allows dual consumption (simple JSX or advanced composition).
The key: Using createElement(children, pieces) instead of children(pieces) allows passing pieces as props to the child function, enabling the render props pattern.
Reference: React Render Props documentation
const identify = component =>
Object.assign(component, { displayName: "Custom(GenericButton)" });Why: React DevTools shows meaningful names for dynamically created components.
Reference: displayName for debugging
const GenericButton = ({ children, onClick, ...enhancement }) => {
const Button = props => (
<button {...props} {...enhancement} />
);
};Why: Consumers can override any prop (e.g., disabled, style) without GenericButton needing to know about them.
Reference: Spread syntax, Rest parameters
const pieces = { Button, track, style };Why: track is a utility function exposed as a primitive. Consumers can wrap their own callbacks, even on elements that aren't Button.
// Simple usage (non-function child)
<GenericButton onClick={handler}>Click Me</GenericButton>
// Advanced usage (function child)
<GenericButton onClick={handler}>
{pieces => /* custom composition */}
</GenericButton>Why: Beginners use simple syntax. Advanced users access primitives when needed.
Swizec's article recommends YAGNI: don't abstract until patterns emerge from duplicated code.
"The best time to generalize code is never. The second best time is after you've written the same code 3+ times and deeply understand the pattern."
This codebase agrees with YAGNI but adds a nuance:
When you do abstract, abstract primitives, not configurations.
The difference:
// Configuration abstraction (YAGNI says: wait for duplication)
const GenericButton = ({ variant, size, color }) => { /* ... */ };
// Primitive abstraction (this codebase says: expose pieces early)
const GenericButton = ({ children }) => {
const pieces = { Button, track, style };
const isComponent = object => typeof object === "function";
return isComponent(children)
? createElement(children, pieces)
: <Button>{children}</Button>;
};Why primitive abstraction is safe:
GenericButtondoesn't predict use cases (novariant,size,colorprops)- It provides tools (Button, track, style) not solutions (variants)
- Adding a new use case never requires changing GenericButton
- Composition is pay-as-you-go—simple cases stay simple
This project uses Create React App with React 16.8+ (Hooks support).
With npm:
npm install
npm startOr with Yarn:
yarn install
yarn startOpen the browser console and interact with buttons. Notice:
- Tracking in action: Every button click logs the callback to console
- Composition variety: Some buttons use
Button, some use<input>, some use<p> - Style inheritance: Each button spreads base styles and customizes as needed
- Activation behavior: Toggle button enables/disables other buttons via shared state
| File | Demonstrates |
|---|---|
GenericButton.js |
Primitive exposure pattern |
Activable.js |
Middleware composition layer |
Input.js |
Complex composition with side effects |
Simple.js |
Style override composition |
CloseModal.js |
Style transformation composition |
App.js |
Orchestration and state management |
Swizec's article is right that premature abstraction is dangerous. Where it stops short is in recognizing that the danger is in how you abstract, not whether you abstract.
The configuration-based approach fails because it assumes you can predict flexibility needs:
// This requires predicting the future
<GenericButton variant="?" size="?" color="?" disabled="?" />The composition-based approach succeeds because it makes no predictions:
// This provides tools for consumers to solve their own problems
<GenericButton>
{({ Button, style, track }) => /* consumer decides */}
</GenericButton>The thesis: DRY is not a footgun. Trying to anticipate flexibility through configuration is the footgun. The solution is to expose internal primitives for composition, allowing consumers to build their own solutions from your tools.
This codebase is a proof: you can have reusable components without prop explosion, brittle abstractions, or maintenance nightmares.
- Swizec: "DRY is a footgun, remember to YAGNI" - The article this challenges
- DRY (Don't Repeat Yourself) - Wikipedia
- YAGNI (You Aren't Gonna Need It) - Wikipedia
- SOLID Principles - Including Dependency Inversion
- React Documentation - Official React docs
React.createElement- Creating elements without JSX- React Hooks - useState, useEffect, useCallback
- Render Props - Official pattern documentation
displayName- Component naming for debugging- Kent C. Dodds: "Inversion of Control" - Related composition patterns
- Michael Jackson: "Never Write Another HoC" - Render props vs Higher-Order Components
- Spread syntax - MDN
- Rest parameters - MDN
- Create React App - Zero-config React setup
- React DevTools - Browser debugging extension
- Mermaid - Diagram rendering (used in architecture diagram)
Built with: React 16.8.6, Create React App 2.1.8
License: MIT