React & Frontend

Higher-Order Components em React: Composição e Tipagem Correta na Prática

13 min de leitura

Higher-Order Components em React: Composição e Tipagem Correta na Prática

O que é um Higher-Order Component? 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. 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. Anatomia básica de um HOC 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

<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) =&gt; {

return (props) =&gt; {

return &lt;WrappedComponent {...props} extraProp=&quot;valor&quot; /&gt;;

};

};

// 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) =&gt; {

return (props) =&gt; {

const theme = {

primaryColor: &#039;#3498db&#039;,

secondaryColor: &#039;#2ecc71&#039;

};

return &lt;WrappedComponent {...props} theme={theme} /&gt;;

};

};

// Componente que usa o HOC

const Button = ({ theme, label }) =&gt; (

&lt;button style={{ backgroundColor: theme.primaryColor }}&gt;

{label}

&lt;/button&gt;

);

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) =&gt; (WrappedComponent) =&gt; {

return (props) =&gt; {

const [data, setData] = React.useState(null);

const [loading, setLoading] = React.useState(true);

const [error, setError] = React.useState(null);

React.useEffect(() =&gt; {

fetch(url)

.then(res =&gt; res.json())

.then(data =&gt; {

setData(data);

setLoading(false);

})

.catch(err =&gt; {

setError(err);

setLoading(false);

});

}, [url]);

return (

&lt;WrappedComponent

data={data}

loading={loading}

error={error}

{...props}

/&gt;

);

};

};

// Componente que usa o HOC

const UserList = ({ data, loading, error }) =&gt; {

if (loading) return &lt;p&gt;Carregando...&lt;/p&gt;;

if (error) return &lt;p&gt;Erro: {error.message}&lt;/p&gt;;

return (

&lt;ul&gt;

{data.map(user =&gt; &lt;li key={user.id}&gt;{user.name}&lt;/li&gt;)}

&lt;/ul&gt;

);

};

const UserListWithData = withDataFetch(&#039;https://api.example.com/users&#039;)(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) =&gt; (props) =&gt; (

&lt;Component {...props} theme={{ color: &#039;blue&#039; }} /&gt;

);

const withLogging = (Component) =&gt; (props) =&gt; {

React.useEffect(() =&gt; {

console.log(&#039;Componente montado&#039;);

return () =&gt; console.log(&#039;Componente desmontado&#039;);

}, []);

return &lt;Component {...props} /&gt;;

};

// Composição em cadeia

const MyComponent = ({ theme }) =&gt; &lt;div style={{ color: theme.color }}&gt;Olá&lt;/div&gt;;

const Enhanced = withLogging(withTheme(MyComponent));

// Ou criar uma função utilitária de composição

const compose = (...fns) =&gt; (component) =&gt;

fns.reduceRight((acc, fn) =&gt; 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 &#039;react&#039;;

// Define as props que o HOC adiciona

interface WithThemeProps {

theme: {

primaryColor: string;

secondaryColor: string;

};

}

// HOC genérico tipado

const withTheme = &lt;P extends object&gt;(

WrappedComponent: ComponentType&lt;P &amp; WithThemeProps&gt;

): FC&lt;P&gt; =&gt; {

return (props: P) =&gt; {

const theme = {

primaryColor: &#039;#3498db&#039;,

secondaryColor: &#039;#2ecc71&#039;

};

return &lt;WrappedComponent {...props} theme={theme} /&gt;;

};

};

// Componente que recebe tanto props originais quanto do HOC

interface ButtonProps {

label: string;

onClick?: () =&gt; void;

}

const Button: FC&lt;ButtonProps &amp; WithThemeProps&gt; = ({

label,

onClick,

theme

}) =&gt; (

&lt;button

onClick={onClick}

style={{ backgroundColor: theme.primaryColor }}

&gt;

{label}

&lt;/button&gt;

);

const ThemedButton = withTheme(Button);

// Uso - TypeScript sabe que precisa de &#039;label&#039; mas não de &#039;theme&#039;

&lt;ThemedButton label=&quot;Clique aqui&quot; onClick={() =&gt; console.log(&#039;clicado&#039;)} /&gt;;</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 &#039;react&#039;;

interface WithDataFetchProps&lt;T&gt; {

data: T | null;

loading: boolean;

error: Error | null;

}

const withDataFetch = &lt;T,&gt;(url: string) =&gt; &lt;P extends object&gt;(

WrappedComponent: ComponentType&lt;P &amp; WithDataFetchProps&lt;T&gt;&gt;

): FC&lt;P&gt; =&gt; {

return (props: P) =&gt; {

const [data, setData] = useState&lt;T | null&gt;(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState&lt;Error | null&gt;(null);

useEffect(() =&gt; {

fetch(url)

.then(res =&gt; res.json())

.then((data: T) =&gt; {

setData(data);

setLoading(false);

})

.catch(err =&gt; {

setError(err);

setLoading(false);

});

}, [url]);

return (

&lt;WrappedComponent

{...props}

data={data}

loading={loading}

error={error}

/&gt;

);

};

};

// Definir tipo de resposta

interface User {

id: number;

name: string;

email: string;

}

interface UserListProps {

category?: string;

}

const UserList: FC&lt;UserListProps &amp; WithDataFetchProps&lt;User[]&gt;&gt; = ({

data,

loading,

error,

category

}) =&gt; {

if (loading) return &lt;p&gt;Carregando...&lt;/p&gt;;

if (error) return &lt;p&gt;Erro: {error.message}&lt;/p&gt;;

return (

&lt;div&gt;

{category &amp;&amp; &lt;h2&gt;{category}&lt;/h2&gt;}

&lt;ul&gt;

{data?.map(user =&gt; (

&lt;li key={user.id}&gt;{user.name}&lt;/li&gt;

))}

&lt;/ul&gt;

&lt;/div&gt;

);

};

const UserListWithData = withDataFetch&lt;User[]&gt;(

&#039;https://api.example.com/users&#039;

)(UserList);

// TypeScript sabe que &#039;category&#039; é opcional

&lt;UserListWithData category=&quot;Administradores&quot; /&gt;;</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 &#039;hoist-non-react-statics&#039;;

const withExample = &lt;P extends object&gt;(

WrappedComponent: React.ComponentType&lt;P&gt;

): React.FC&lt;P&gt; =&gt; {

const Wrapper: React.FC&lt;P&gt; = (props) =&gt; {

return &lt;WrappedComponent {...props} /&gt;;

};

// Copia propriedades estáticas

hoistNonReactStatics(Wrapper, WrappedComponent);

// Define displayName útil para debugging

Wrapper.displayName = `withExample(${

WrappedComponent.displayName || WrappedComponent.name || &#039;Component&#039;

})`;

return Wrapper;

};

// Uso

const MyComponent = () =&gt; &lt;div&gt;Olá&lt;/div&gt;;

MyComponent.defaultProps = { message: &#039;default&#039; };

const Enhanced = withExample(MyComponent);

// Enhanced.defaultProps ainda está acessível

// E o displayName será &#039;withExample(MyComponent)&#039; 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 &#039;react&#039;;

interface InputHandle {

focus: () =&gt; void;

clear: () =&gt; void;

}

const withInputControl = &lt;P extends object&gt;(

WrappedComponent: React.ComponentType&lt;P &amp; { ref: React.Ref&lt;InputHandle&gt; }&gt;

) =&gt; {

return forwardRef&lt;InputHandle, P&gt;((props, ref) =&gt; {

const inputRef = useRef&lt;HTMLInputElement&gt;(null);

useImperativeHandle(ref, () =&gt; ({

focus: () =&gt; inputRef.current?.focus(),

clear: () =&gt; {

if (inputRef.current) {

inputRef.current.value = &#039;&#039;;

}

}

}), []);

return &lt;WrappedComponent {...props} ref={inputRef} /&gt;;

});

};

interface TextInputProps {

placeholder: string;

ref?: React.Ref&lt;HTMLInputElement&gt;;

}

const TextInput = forwardRef&lt;HTMLInputElement, TextInputProps&gt;(

({ placeholder }, ref) =&gt; (

&lt;input ref={ref} placeholder={placeholder} /&gt;

)

);

const ControlledInput = withInputControl(TextInput);

// Uso

const App = () =&gt; {

const inputRef = useRef&lt;InputHandle&gt;(null);

return (

&lt;div&gt;

&lt;ControlledInput placeholder=&quot;Digite algo&quot; ref={inputRef} /&gt;

&lt;button onClick={() =&gt; inputRef.current?.focus()}&gt;Focar&lt;/button&gt;

&lt;button onClick={() =&gt; inputRef.current?.clear()}&gt;Limpar&lt;/button&gt;

&lt;/div&gt;

);

};</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: &#039;light&#039; | &#039;dark&#039;;

customColor?: string;

}

interface WithThemeProps {

theme: ThemeConfig &amp; { apply: (key: string) =&gt; string };

}

const createThemeHOC = (config: ThemeConfig) =&gt; &lt;P extends object&gt;(

WrappedComponent: React.ComponentType&lt;P &amp; WithThemeProps&gt;

): React.FC&lt;P&gt; =&gt; {

return (props: P) =&gt; {

const theme = {

...config,

apply: (key: string) =&gt; {

if (key === &#039;bg&#039;) {

return config.mode === &#039;dark&#039; ? &#039;#1a1a1a&#039; : &#039;#ffffff&#039;;

}

return config.customColor || &#039;#3498db&#039;;

}

};

return &lt;WrappedComponent {...props} theme={theme} /&gt;;

};

};

// Uso

const lightThemeHOC = createThemeHOC({ mode: &#039;light&#039; });

const darkThemeHOC = createThemeHOC({

mode: &#039;dark&#039;,

customColor: &#039;#ff6b6b&#039;

});

const MyComponent: React.FC&lt;WithThemeProps&gt; = ({ theme }) =&gt; (

&lt;div style={{ backgroundColor: theme.apply(&#039;bg&#039;) }}&gt;

Conteúdo

&lt;/div&gt;

);

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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI
Guia Completo de React Query Avançado: Cache, Stale Time, Prefetch e Optimistic UI

Entendendo o Cache e sua Importância no React Query O cache é o coração do Re...

Streaming SSR com React: Suspense no Servidor e Progressive Hydration na Prática
Streaming SSR com React: Suspense no Servidor e Progressive Hydration na Prática

O Que É Streaming SSR com React? Streaming SSR (Server-Side Rendering com Str...

Como Usar Bundle Analysis em React: Webpack Bundle Analyzer e Tree Shaking em Produção
Como Usar Bundle Analysis em React: Webpack Bundle Analyzer e Tree Shaking em Produção

Entendendo Bundle Analysis e sua Importância Bundle analysis é o processo de...