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.

JavaScript
Frontend
4 snippets
react
higher-order-functions
design-patterns
references
diyahassan

By @diyahassan

March 7, 2026

·

Updated May 20, 2026

808 views

20

4.2 (11)

// withCounter is the textbook HOC: it accepts a component and returns a new
// component that injects extra props (count and increment). The wrapped
// component cannot tell if its count came from local state or from a parent.
// We need a class harness because the wrapped component is a class; in real
// React this is just `extends React.Component`.

class Component {
    constructor(props) { this.props = props || {}; this.state = {}; }
    setState(patch) { this.state = Object.assign({}, this.state, typeof patch === 'function' ? patch(this.state) : patch); this._render && this._render(); }
}

// The HOC. Wraps any component and gives it { count, increment } props.
function withCounter(Wrapped, initial) {
    return class WithCounter extends Component {
        constructor(props) {
            super(props);
            this.state = { count: initial || 0 };
            this.increment = () => this.setState((s) => ({ count: s.count + 1 }));
        }
        render() {
            const enhanced = Object.assign({}, this.props, {
                count: this.state.count,
                increment: this.increment,
            });
            const inner = new Wrapped(enhanced);
            inner.props = enhanced;
            return inner.render();
        }
    };
}

// A consumer that knows nothing about counter state.
class ClickCounter extends Component {
    render() {
        const { count, increment, label } = this.props;
        return { type: 'button', props: { onClick: increment }, children: [label + ': ' + count] };
    }
}

const Wrapped = withCounter(ClickCounter, 5);
const instance = new Wrapped({ label: 'clicks' });
const tree1 = instance.render();
console.log('initial render:', tree1.children[0]);
console.log('button props -> count injected, increment is a function:',
    'count' in tree1.props ? 'no (in children)' : 'count in injected props',
    typeof instance.increment);
instance.increment();
const tree2 = instance.render();
console.log('after increment:', tree2.children[0]);
instance.increment();
instance.increment();
const tree3 = instance.render();
console.log('after two more :', tree3.children[0]);

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.