<h2>O que é um Higher-Order Component?</h2>
<p>Um Higher-Order Component (HOC) é um padrão avançado em React que permite compartilhar lógica entre componentes. Tecnicamente, um HOC é uma função que recebe um componente como argumento e retorna um novo componente aprimorado com funcionalidade adicional. Diferente de hooks (que são mais modernos), HOCs trabalham com a composição de componentes e são especialmente úteis quando você precisa reutilizar lógica em múltiplos componentes sem modificar a estrutura interna deles.</p>
<p>A razão pela qual estudamos HOCs ainda hoje, apesar dos hooks terem se tornado mais populares, é que muitas bases de código legado utilizam este padrão, e compreendê-lo é essencial para manutenção e evolução de projetos. Além disso, HOCs continuam sendo uma escolha válida em certos cenários e facilitam entender padrões mais complexos de composição em React.</p>
<h3>Anatomia básica de um HOC</h3>
<p>Um HOC segue uma estrutura bem definida. Você cria uma função que recebe um componente, envolve-o em outro componente e retorna este novo componente que pode passar props adicionais, gerenciar estado, ou executar efeitos colaterais.</p>
<pre><code class="language-javascript">// Estrutura básica de um HOC
const withEnhancement = (WrappedComponent) => {
return (props) => {
return <WrappedComponent {...props} extraProp="valor" />;
};
};
// Uso
const EnhancedComponent = withEnhancement(MyComponent);</code></pre>
<p>Essa simplicidade é enganosa. Na prática, você precisará lidar com propriedades estáticas do componente original, displayName correto, e claro, tipagem TypeScript robusta. Um HOC mal implementado pode causar problemas de performance e debugging difícil.</p>
<h2>Padrões de Composição em HOCs</h2>
<h3>Composição simples vs composição complexa</h3>
<p>Existem diferentes níveis de composição em HOCs. A composição simples ocorre quando você apenas passa props adicionais ou injeta um valor. Composição complexa acontece quando você precisa interceptar ciclos de vida, gerenciar estado interno, ou combinar múltiplos HOCs.</p>
<p>Para composição simples, basta envolver o componente e expandir as props:</p>
<pre><code class="language-javascript">// HOC simples que injeta um tema
const withTheme = (WrappedComponent) => {
return (props) => {
const theme = {
primaryColor: '#3498db',
secondaryColor: '#2ecc71'
};
return <WrappedComponent {...props} theme={theme} />;
};
};
// Componente que usa o HOC
const Button = ({ theme, label }) => (
<button style={{ backgroundColor: theme.primaryColor }}>
{label}
</button>
);
const ThemedButton = withTheme(Button);</code></pre>
<p>Para composição complexa, você geralmente precisa gerenciar estado e ciclos de vida no HOC:</p>
<pre><code class="language-javascript">// HOC complexo que gerencia estado e chamadas HTTP
const withDataFetch = (url) => (WrappedComponent) => {
return (props) => {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return (
<WrappedComponent
data={data}
loading={loading}
error={error}
{...props}
/>
);
};
};
// Componente que usa o HOC
const UserList = ({ data, loading, error }) => {
if (loading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error.message}</p>;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
};
const UserListWithData = withDataFetch('https://api.example.com/users')(UserList);</code></pre>
<h3>Composição em cadeia de múltiplos HOCs</h3>
<p>Frequentemente você precisa aplicar múltiplos HOCs a um único componente. Isso é chamado composição em cadeia:</p>
<pre><code class="language-javascript">// Vários HOCs
const withTheme = (Component) => (props) => (
<Component {...props} theme={{ color: 'blue' }} />
);
const withLogging = (Component) => (props) => {
React.useEffect(() => {
console.log('Componente montado');
return () => console.log('Componente desmontado');
}, []);
return <Component {...props} />;
};
// Composição em cadeia
const MyComponent = ({ theme }) => <div style={{ color: theme.color }}>Olá</div>;
const Enhanced = withLogging(withTheme(MyComponent));
// Ou criar uma função utilitária de composição
const compose = (...fns) => (component) =>
fns.reduceRight((acc, fn) => fn(acc), component);
const Enhanced2 = compose(withLogging, withTheme)(MyComponent);</code></pre>
<h2>Tipagem Correta com TypeScript</h2>
<h3>Entendendo genéricos em HOCs</h3>
<p>TypeScript exige que você declare explicitamente que tipos de props o componente original aceita e que tipos adicionais o HOC adiciona. Isso é feito através de genéricos. Um HOC sem tipagem correta perde completamente o benefício do type-checking e se torna mais problemático que útil.</p>
<pre><code class="language-typescript">import React, { FC, ComponentType } from 'react';
// Define as props que o HOC adiciona
interface WithThemeProps {
theme: {
primaryColor: string;
secondaryColor: string;
};
}
// HOC genérico tipado
const withTheme = <P extends object>(
WrappedComponent: ComponentType<P & WithThemeProps>
): FC<P> => {
return (props: P) => {
const theme = {
primaryColor: '#3498db',
secondaryColor: '#2ecc71'
};
return <WrappedComponent {...props} theme={theme} />;
};
};
// Componente que recebe tanto props originais quanto do HOC
interface ButtonProps {
label: string;
onClick?: () => void;
}
const Button: FC<ButtonProps & WithThemeProps> = ({
label,
onClick,
theme
}) => (
<button
onClick={onClick}
style={{ backgroundColor: theme.primaryColor }}
>
{label}
</button>
);
const ThemedButton = withTheme(Button);
// Uso - TypeScript sabe que precisa de 'label' mas não de 'theme'
<ThemedButton label="Clique aqui" onClick={() => console.log('clicado')} />;</code></pre>
<h3>HOCs com múltiplos genéricos e composição</h3>
<p>Quando você precisa de um HOC que gerencia estado ou faz requisições, a tipagem fica mais sofisticada:</p>
<pre><code class="language-typescript">import React, { FC, ComponentType, useState, useEffect } from 'react';
interface WithDataFetchProps<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
const withDataFetch = <T,>(url: string) => <P extends object>(
WrappedComponent: ComponentType<P & WithDataFetchProps<T>>
): FC<P> => {
return (props: P) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((data: T) => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return (
<WrappedComponent
{...props}
data={data}
loading={loading}
error={error}
/>
);
};
};
// Definir tipo de resposta
interface User {
id: number;
name: string;
email: string;
}
interface UserListProps {
category?: string;
}
const UserList: FC<UserListProps & WithDataFetchProps<User[]>> = ({
data,
loading,
error,
category
}) => {
if (loading) return <p>Carregando...</p>;
if (error) return <p>Erro: {error.message}</p>;
return (
<div>
{category && <h2>{category}</h2>}
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
};
const UserListWithData = withDataFetch<User[]>(
'https://api.example.com/users'
)(UserList);
// TypeScript sabe que 'category' é opcional
<UserListWithData category="Administradores" />;</code></pre>
<h3>Preservando propriedades estáticas e displayName</h3>
<p>Um erro comum é perder propriedades estáticas do componente original ao envolvê-lo em um HOC. Use a biblioteca <code>hoist-non-react-statics</code>:</p>
<pre><code class="language-typescript">import hoistNonReactStatics from 'hoist-non-react-statics';
const withExample = <P extends object>(
WrappedComponent: React.ComponentType<P>
): React.FC<P> => {
const Wrapper: React.FC<P> = (props) => {
return <WrappedComponent {...props} />;
};
// Copia propriedades estáticas
hoistNonReactStatics(Wrapper, WrappedComponent);
// Define displayName útil para debugging
Wrapper.displayName = `withExample(${
WrappedComponent.displayName || WrappedComponent.name || 'Component'
})`;
return Wrapper;
};
// Uso
const MyComponent = () => <div>Olá</div>;
MyComponent.defaultProps = { message: 'default' };
const Enhanced = withExample(MyComponent);
// Enhanced.defaultProps ainda está acessível
// E o displayName será 'withExample(MyComponent)' no DevTools</code></pre>
<h2>Padrões Avançados e Boas Práticas</h2>
<h3>Ref forwarding em HOCs</h3>
<p>Ao envolver um componente com um HOC, refs não são automaticamente passadas. Você precisa usar <code>forwardRef</code> e <code>useImperativeHandle</code>:</p>
<pre><code class="language-typescript">import React, { forwardRef, useImperativeHandle, useRef } from 'react';
interface InputHandle {
focus: () => void;
clear: () => void;
}
const withInputControl = <P extends object>(
WrappedComponent: React.ComponentType<P & { ref: React.Ref<InputHandle> }>
) => {
return forwardRef<InputHandle, P>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}), []);
return <WrappedComponent {...props} ref={inputRef} />;
});
};
interface TextInputProps {
placeholder: string;
ref?: React.Ref<HTMLInputElement>;
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ placeholder }, ref) => (
<input ref={ref} placeholder={placeholder} />
)
);
const ControlledInput = withInputControl(TextInput);
// Uso
const App = () => {
const inputRef = useRef<InputHandle>(null);
return (
<div>
<ControlledInput placeholder="Digite algo" ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focar</button>
<button onClick={() => inputRef.current?.clear()}>Limpar</button>
</div>
);
};</code></pre>
<h3>HOCs com configuração via factory pattern</h3>
<p>Às vezes você quer que o HOC seja configurável. Use o padrão factory:</p>
<pre><code class="language-typescript">interface ThemeConfig {
mode: 'light' | 'dark';
customColor?: string;
}
interface WithThemeProps {
theme: ThemeConfig & { apply: (key: string) => string };
}
const createThemeHOC = (config: ThemeConfig) => <P extends object>(
WrappedComponent: React.ComponentType<P & WithThemeProps>
): React.FC<P> => {
return (props: P) => {
const theme = {
...config,
apply: (key: string) => {
if (key === 'bg') {
return config.mode === 'dark' ? '#1a1a1a' : '#ffffff';
}
return config.customColor || '#3498db';
}
};
return <WrappedComponent {...props} theme={theme} />;
};
};
// Uso
const lightThemeHOC = createThemeHOC({ mode: 'light' });
const darkThemeHOC = createThemeHOC({
mode: 'dark',
customColor: '#ff6b6b'
});
const MyComponent: React.FC<WithThemeProps> = ({ theme }) => (
<div style={{ backgroundColor: theme.apply('bg') }}>
Conteúdo
</div>
);
const LightVersion = lightThemeHOC(MyComponent);
const DarkVersion = darkThemeHOC(MyComponent);</code></pre>
<h3>Quando NÃO usar HOCs</h3>
<p>É importante reconhecer que em muitos cenários modernos, hooks são uma escolha superior aos HOCs. Use hooks quando quiser compartilhar lógica de estado ou efeitos entre componentes funcionais. Use HOCs apenas quando precisar envolver um componente inteiro com comportamento transversal, como autenticação, logging ou tema global. Avoid HOCs para casos simples que hooks resolvem de forma mais clara e com menos overhead.</p>
<h2>Conclusão</h2>
<p>Três aprendizados principais resumem este artigo: primeiro, um HOC é fundamentalmente uma função que retorna um componente, permitindo reutilização de lógica através de composição, não herança. Segundo, tipagem TypeScript com genéricos e <code>ComponentType</code> é não-negociável em projetos sérios, e sem ela você perde o principal benefício da linguagem. Terceiro, HOCs compartilham espaço com hooks na caixa de ferramentas moderna do React — conhecer ambos e saber quando usar cada um é o que diferencia um desenvolvedor competente de um iniciante. A combinação de composição clara, tipagem robusta e compreensão dos trade-offs entre HOCs e hooks permite código manutenível, escalável e com excelente experiência de desenvolvimento.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://react.dev/reference/react/createElement#creating-a-component-with-hoc" target="_blank" rel="noopener noreferrer">React Official Documentation - Higher-Order Components</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/types-from-types.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Advanced Types and Generics</a></li>
<li><a href="https://www.npmjs.com/package/hoist-non-react-statics" target="_blank" rel="noopener noreferrer">hoist-non-react-statics NPM Package</a></li>
<li><a href="https://medium.com/@dan_abramov/mixins-are-dead-long-live-composition-f4a642656d42" target="_blank" rel="noopener noreferrer">Dan Abramov - Higher-Order Components in Depth</a></li>
<li><a href="https://reactpatterns.com/" target="_blank" rel="noopener noreferrer">React Patterns - Higher-Order Components</a></li>
</ul>
<p><!-- FIM --></p>