Chapter 10: Patterns and Anti-Patterns: The Good, The Bad, The React
React patterns are like fashion trends. What’s hot today is tomorrow’s code smell. What was an anti-pattern last year is this year’s best practice. And just like fashion, everyone pretends they always knew bell-bottoms would come back.
Container/Presentational: Separation That Isn’t
Remember when this was the gold standard?
// Container Component (Smart)
class UserListContainer extends React.Component {
state = { users: [] };
componentDidMount() {
fetch('/api/users')
.then(res => res.json())
.then(users => this.setState({ users }));
}
render() {
return <UserList users={this.state.users} />;
}
}
// Presentational Component (Dumb)
const UserList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
“Separation of concerns!” we said. “Smart components handle logic, dumb components handle presentation!”
Then hooks came along:
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
Suddenly, the pattern we swore by for years was an anti-pattern. Separation of concerns became “why are you creating two components for no reason?”
Higher-Order Components: Inception for Code
HOCs were React’s way of sharing logic before hooks. It’s a function that takes a component and returns a component. If that sounds confusing, wait until you see it in action:
// The HOC
const withAuth = (WrappedComponent) => {
return class extends React.Component {
state = { user: null };
componentDidMount() {
// Auth logic
this.setState({ user: getCurrentUser() });
}
render() {
return <WrappedComponent {...this.props} user={this.state.user} />;
}
};
};
// Usage
const ProfilePage = ({ user }) => <div>Welcome, {user.name}!</div>;
const AuthProfilePage = withAuth(ProfilePage);
// Or with multiple HOCs (HOC hell)
export default withAuth(
withRouter(
withTheme(
withLocale(
withAnalytics(
withErrorBoundary(
ProfilePage
)
)
)
)
)
);
That’s six levels of wrapping. Your component is now a Russian doll of functions returning functions returning components.
Render Props: Functions Returning Functions Returning JSX
When HOCs weren’t confusing enough, we invented render props:
class Mouse extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY });
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
// Usage
<Mouse render={({ x, y }) => (
<div>The mouse is at ({x}, {y})</div>
)} />
We’re passing functions as props that return JSX. It’s like callback hell and component composition had a baby.
Compound Components: When Simple Gets Complicated
Compound components seem elegant:
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanels>
<TabPanel>Panel 1</TabPanel>
<TabPanel>Panel 2</TabPanel>
</TabPanels>
</Tabs>
But implementing them requires:
- React.Children.map
- Context API
- cloneElement
- Implicit parent-child contracts
- Prayer that someone doesn’t rearrange the children
const Tabs = ({ children }) => {
const [activeIndex, setActiveIndex] = useState(0);
return (
<TabContext.Provider value=>
{React.Children.map(children, (child, index) =>
React.cloneElement(child, { index })
)}
</TabContext.Provider>
);
};
Congratulations, you’ve made tabs complicated.
The useReducer Pattern: Redux Envy
“useState is too simple!” said nobody ever. But React gave us useReducer anyway:
const formReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return initialState;
case 'SUBMIT':
return { ...state, isSubmitting: true };
default:
return state;
}
};
const Form = () => {
const [state, dispatch] = useReducer(formReducer, initialState);
return (
<form onSubmit={() => dispatch({ type: 'SUBMIT' })}>
<input
value={state.name}
onChange={(e) => dispatch({
type: 'UPDATE_FIELD',
field: 'name',
value: e.target.value
})}
/>
</form>
);
};
We’ve turned setState({ name: value })
into a 30-line state machine. Because Redux worked so well, right?
The Custom Hook Everything Pattern
Once developers discovered custom hooks, everything became a hook:
// Before hooks: Simple function
function formatDate(date) {
return date.toLocaleDateString();
}
// After hooks: Everything must be a hook!
function useFormattedDate(date) {
const [formatted, setFormatted] = useState('');
useEffect(() => {
setFormatted(date.toLocaleDateString());
}, [date]);
return formatted;
}
Congratulations, you’ve made date formatting stateful and asynchronous.
The Over-Memoization Anti-Pattern
“Performance optimization” gone wrong:
const OverOptimized = () => {
// Memoizing a constant
const title = useMemo(() => 'Hello World', []);
// Memoizing a simple calculation
const doubled = useMemo(() => 2 * 2, []);
// Memoizing everything
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
const style = useMemo(() => ({
color: 'red'
}), []);
return (
<div style={style} onClick={handleClick}>
{title} - {doubled}
</div>
);
};
The memoization costs more than the computation it’s preventing.
The God Component Anti-Pattern
When one component does everything:
const App = () => {
// 50 state variables
const [user, setUser] = useState();
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
const [likes, setLikes] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// ... 44 more
// 30 useEffects
useEffect(() => { /* fetch user */ }, []);
useEffect(() => { /* fetch posts */ }, [user]);
useEffect(() => { /* fetch comments */ }, [posts]);
// ... 27 more
// 40 handler functions
const handleLogin = () => { /* 50 lines */ };
const handleLogout = () => { /* 30 lines */ };
const handlePostCreate = () => { /* 100 lines */ };
// ... 37 more
// 500 lines of JSX
return (
<div>
{/* Everything renders here */}
</div>
);
};
One component to rule them all, and in the darkness bind them.
The Prop Spreading Catastrophe
const DangerousComponent = (props) => {
return (
<div {...props}> {/* What could go wrong? */}
<button {...props.buttonProps}>
<span {...props.spanProps}>
{props.children}
</span>
</button>
</div>
);
};
// Usage
<DangerousComponent
onClick={handleDivClick} // Goes to div
buttonProps= // Goes to button
spanProps= // Goes to span
// Which handler actually fires? All of them!
/>
The Early Return Maze
const Component = ({ user, posts, permissions }) => {
if (!user) return <Login />;
if (!permissions) return <NoPermissions />;
if (!posts) return <Loading />;
if (posts.length === 0) return <NoPosts />;
if (user.banned) return <Banned />;
if (!user.emailVerified) return <VerifyEmail />;
if (maintenanceMode) return <Maintenance />;
// The actual component, 7 guards deep
return <div>Content</div>;
};
Your component is more security checkpoint than component.
The State Initialization Function Anti-Pattern
// Wrong: Function runs every render
const Component = () => {
const [data, setData] = useState(expensiveComputation());
};
// Right: Function runs once
const Component = () => {
const [data, setData] = useState(() => expensiveComputation());
};
// But people do this anyway
const Component = () => {
const [data, setData] = useState(() => {
// 100 lines of initialization logic
// Making useState do way too much
return complexData;
});
};
In Defense of Patterns (Some of Them)
Some patterns are actually useful:
- Composition over Inheritance: Actually good advice
- Single Responsibility: When actually followed
- Custom Hooks: For truly reusable logic
- Error Boundaries: Because errors happen
But we’ve taken every pattern to its extreme, turning guidelines into gospel.
The Path Forward
You’re going to use these patterns. You’re going to create HOCs that you’ll refactor into hooks. You’re going to over-memoize, then under-memoize, then give up and use a state management library.
And eventually, you’ll realize that the best pattern is often no pattern - just simple, readable code that does what it needs to do.
In the next chapter, we’ll explore React performance myths - why your app is slow, and why it’s probably not for the reasons you think.
But first, appreciate the irony that we’ve created patterns to manage the complexity that the patterns themselves create. It’s patterns all the way down, and confusion all the way up.