Hooks replaced many inappropriate HOC use cases. They did not remove the need to apply independent rules around an existing rendering target.
When we ask whether HOCs are obsolete, it is easy to mix up two different questions:
- How should components reuse internal state, requests, subscriptions, and interaction logic?
- How should an application independently handle props,
refs, layout, and presentation policies around the rendering boundary of an existing component or native element?
For the first question, a custom Hook is usually the right starting point today. If an HOC only moves useQuery, useEffect, or useState outward and passes the result back through props, it often adds little more than another component layer and prop forwarding.
The second question did not disappear with Hooks. The key is not whether a few lines of code can live inside a component. It is this:
Should this rule be owned by the target component itself, or should it remain an optional, replaceable, and composable rendering policy?
Hooks are for reusing internal logic
Custom Hooks are the natural modern React mechanism for reusing stateful and side-effectful logic. If several components need to subscribe to online status, for example, that subscription can live in useOnlineStatus(). Each calling component still decides how to use the result and what UI to render.
The ownership stays inside the caller's boundary: the component uses the data, handles the interaction, and owns the final output. A Hook does not introduce a new UI boundary, which is why it fits data access, state, subscriptions, and event handling so well.
This also explains why many classic HOCs now feel cumbersome: they were often used for internal logic reuse that a Hook can express more directly.
A Hook, however, is not a universal syntax for wrapping rendering elsewhere. If a requirement needs to alter props passed to a target, intercept a ref, or place the rendered result inside a shared layout, it is dealing with a different boundary.
Start with React: what does an autofocus HOC solve?
Consider an input that should receive focus after it mounts. If we want that behavior to be attachable to multiple input-compatible components, a traditional React HOC might look like this:
function withAutoFocus(WrappedInput) {
return React.forwardRef(function AutoFocus(props, outerRef) {
const inputRef = React.useRef(null);
React.useEffect(() => {
inputRef.current?.focus();
}, []);
return (
<WrappedInput
{...props}
ref={node => {
inputRef.current = node;
if (typeof outerRef === 'function') {
outerRef(node);
} else if (outerRef) {
outerRef.current = node;
}
}}
/>
);
});
}
Used like this:
const AutoFocusInput = withAutoFocus(TextInput);
<AutoFocusInput placeholder="Enter something" />
This works, and the requirement it expresses is perfectly valid: add a “focus after mount” rule to a render target without changing TextInput itself.
It also exposes the cost of the classic HOC expression:
- it creates a new component type;
- if callers need the input node, it must deliberately preserve the outer ref contract;
- the wrapped component must support the relevant ref contract too;
- production HOCs often also need to consider debugging names and static properties.
So the useful conclusion is not that HOCs can never be used. It is that an HOC is rarely the first choice when the goal is only to reuse internal stateful logic; when the rule really belongs at a rendering boundary, it is still describing a real problem.
The decision test: who owns this rule?
When a requirement should be reused, start with three questions:
- Is it intrinsic behavior of the target component, or a policy needed only in certain contexts?
- Should callers be able to attach, remove, or combine the rule independently?
- Does it need to alter render input, a
ref, output structure, or presentation around the target?
The answers point to different abstractions:
- If the rule is intrinsic to the component, keep it inside the component, using a Hook where appropriate.
- If it must expose a stable, standalone UI contract, make it a normal component.
- If it only transforms data and has no rendering concern, use a pure helper.
- If it intercepts or wraps an existing render target and must compose independently, it needs some form of wrapping capability.
React HOCs, other wrappers, and Cabloy/Zova Behaviors are different implementations of that last category. They are not the same runtime mechanism, but they all address cases where a rule should not be fused into its target.
Zova Behavior: attach a rule to the render target
In Cabloy/Zova, the same autofocus requirement can be expressed with a Behavior:
<input bs-behaviors-focus type="text" />
The <input> is still just an input. Autofocus is a capability attached to its rendering process. Its essential implementation looks like this:
@Behavior<IBehaviorOptionsFocus>()
export class BehaviorFocus extends BeanBehaviorBase<
IBehaviorOptionsFocus,
IBehaviorPropsInputFocus,
IBehaviorPropsOutputFocus
> {
inputRef?: HTMLElement;
protected render(props, next) {
const refOuter = props?.ref;
props = {
...props,
ref: ref => {
if (this.$options.always || !this.inputRef) {
ref.focus?.();
}
this.inputRef = ref;
refOuter?.(ref);
},
};
return next(props);
}
}
It does a small number of things:
- receives the current render props;
- preserves the original
ref; - injects a new
refthat callsfocus()once the element is available; - calls the original
refas well; - continues the original render with
next(props).
This is direct render-time composition: a Behavior can adjust the input to the next render step, while the target element remains unaware of whether autofocus is needed.
Behavior is not a “Vue HOC.” Zova is built on Vue runtime and reactivity, but it provides an application architecture based on controllers, beans, and IoC. Behavior is Zova's native facility for render-time interception and composition. The important point here is not its lower-level runtime machinery, but that it expresses autofocus as a rule that can be attached to a target independently.
Not only props: wrapping rendered output
Autofocus demonstrates a before-render shape: change the input to the next render step, such as props or a ref.
A second kind of requirement makes the value of wrapping even clearer: form-field layout. A field control owns input, values, and validation, but different pages may need to present it differently: add a label, display validation errors, insert prefix or suffix content, or apply a block or inline layout.
A layout Behavior can first obtain the field's original rendering result, then add presentation structure around it. Its core shape is like this:
const vnode = next(renderContext);
return (
<fieldset>
<legend>{label}</legend>
{vnode}
{errorMessage}
</fieldset>
);
If the layout rule is intended only for a native input, a separate layout Behavior can be attached directly to <input>. For example, define demo-ui:inputLayout to accept ordinary native props, call const vnode = next(props), and return a layout shell. Its usage can be as simple as:
<input bs-demo-ui-inputLayout type="text" placeholder="Enter something" />
That attaches demo-ui:inputLayout to the rendering process of a native input: the Behavior continues rendering the input, then places its resulting vnode in its own layout structure. It does not change the responsibility of <input>, and it does not require a dedicated input component for this case.
Cabloy Basic's existing form-field layout Behavior follows the same “render first, then wrap” pattern, but it runs inside a form-field host and depends on field state and layout configuration. It should therefore be used through ZFormField and a form provider, not attached directly to an arbitrary <input>. Native-element use cases need a separate Behavior such as demo-ui:inputLayout that handles ordinary input props.
A page can also choose a form-field layout Behavior. For example, the login page selects home-login:formFieldLayoutLogin through ZForm's formProvider, so every field in that form automatically receives the layout Behavior and renders with a consistent visual style:
<ZForm
formProvider={{
behaviors: {
FormFieldLayout: 'home-login:formFieldLayoutLogin',
},
}}
>
{/* fields */}
</ZForm>
The useful separation is: the field owns the field itself; the layout policy owns how that field is wrapped and presented in this page.
If the login layout were hard-coded into every field component, layout policies for different pages would contaminate one another. If every page copied fields and layout code, there would be no shared point of adjustment. A selectable rendering rule preserves the field's stable contract while preserving page-level presentation freedom.
Which reuse mechanism should you choose?
| Need | First choice | Why |
|---|---|---|
| Reuse state, data, subscriptions, and interaction inside a component | Custom Hook | The logic still belongs to the calling component's own boundary. |
| Provide stable, standalone UI structure with explicit inputs and outputs | Component | The abstraction owns a clear UI contract. |
| Transform data or normalize options without rendering context | Pure helper | No framework-level render composition is needed. |
Alter props/refs or wrap an existing render target |
HOC / Wrapper / Behavior | The rule belongs around the target and may need independent attachment and composition. |
This table does not mean every visual requirement should become a Behavior. When an abstraction owns a stable interface and interaction contract of its own, a normal component is usually clearer. HOCs, wrappers, and Behaviors matter because they let a rule live around an existing target instead of becoming an inseparable part of that target.
Conclusion
Classic React HOCs can seem obsolete mainly because they were once used to solve many problems that Hooks now solve better.
Wrapping itself is not obsolete. As long as a requirement is fundamentally about:
- intercepting a target's props or
refbefore render; - adding a shared presentation or interaction policy outside the target;
- independently declaring and combining cross-cutting rules;
there is still a place for HOCs, wrappers, and Zova Behaviors.
What became outdated was not the idea of wrapping. It was the habit of turning every reuse problem into an HOC.\
Hooks are for internal logic reuse; wrapping capabilities attach and compose cross-cutting rules at a rendering boundary.
Top comments (0)