Community JavaScript Snippet
The HOC + Render Props Patterns I Still Read in Legacy Repos
Hooks made HOCs and render props optional, but pre-2019 codebases still ship them. Four patterns to recognize when you inherit a Redux-era React app.
The HOC + Render Props Patterns I Still Read in Legacy Repos
Hooks made HOCs and render props optional, but pre-2019 codebases still ship them. Four patterns to recognize when you inherit a Redux-era React app.
By @diyahassan
March 7, 2026
·
Updated May 20, 2026
808 views
20
4.2 (11)
The contract is what makes HOCs unfamiliar to anyone who started on hooks. The HOC owns a slice of state, the wrapped component receives that state as props, and the wrapped component never knows the state lives somewhere else. The two practical wins were that the consumer stayed a pure render function and that you could compose multiple HOCs onto the same component, which is how compose(withRouter, connect(mapState, mapDispatch))(MyView) came to be the canonical Redux + react-router-v4 wrapping. The cost is that the wrapper hierarchy shows up in React DevTools as WithCounter(ClickCounter), debugging stack traces get noisy, and prop collisions (HOC overwriting a prop the consumer already had) are silent unless you go out of your way to detect them. Hooks did away with both costs.
Render props express the same data flow as a HOC but at the call site. You write <Counter render={({ count, increment }) => <Button onClick={increment}>{count}</Button>}> and the function-as-prop is what consumes the injected state. The function-as-children variant (the second example) is more ergonomic in JSX because you can write <Counter>{({ count }) => ...}</Counter> and the JSX nesting reads naturally. The pattern is more flexible than HOCs because the consumer can pull only the props it wants out of the destructured argument and the wrapper hierarchy stays flat, but the inline function is a fresh allocation per render, which is the entire reason later libraries pivoted to hooks once those landed.
Props proxies are the underrated HOC variant: they exist because not every wrapper wants to inject state, sometimes the only job is to map an incoming prop shape into the shape the inner component already understands. Theming layers are the canonical use case (<ThemedButton variant="primary" theme="dark" /> becomes a <Button className="btn-primary theme-dark" /> on the DOM), and feature-flag shims that disable interactive elements when a flag is on are the second canonical case. The filter callback is what stops noise leaking through to the DOM: variant and theme are component-level props, not HTML attributes, so we drop them on the way down rather than letting React warn at runtime. In modern code I would write this as a wrapper component plus props destructuring, but the HOC version still exists in any codebase old enough to have a styled-components v3 dependency.
Anyone who has inherited a 2018-era component library has hit this exact bug: a parent attaches a ref to a <LoggedInput ref={inputRef} /> and inputRef.current is null at runtime. Refs are not props, so a wrapper that only spreads props swallows them. React.forwardRef is the official escape hatch: the wrapper accepts (props, ref) and forwards the ref to the inner component, where it lands on whatever DOM node or class instance was the original target. The contract is mechanical, but it is invisible until you need it, and it shows up in any HOC that wraps an input, a button, a focus manager, or anything else parents might want to call imperative methods on. In modern code you can usually use useImperativeHandle instead, but legacy class-based HOCs still need this exact wrapping.
