React: Composição e reutilização de componentes
Última atualização: 2026-08-26
Combinar componentes é como brincar de Lego — nem todas as peças precisam ser únicas. Um bom projeto de componentes significa que os blocos básicos (subcomponentes) são flexíveis e podem ser montados de várias maneiras para criar qualquer forma.
1. O que você vai aprender
- Composição x Herança
- Slots infantis e modo multislot
- Princípios da decomposição de componentes (responsabilidade única)
- Padrão Render Props
- Introdução aos Componentes de Ordem Superior (HOCs)
2. A história de um sistema de design
(1) Desafio: Reinventar a roda em cada página
Charlie é o arquiteto de front-end da empresa. Ele percebeu que todas as páginas estavam reinventando a roda:
// Page A:Custom Pop-up Window
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '24px', borderRadius: '8px' }}>
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete this record??</p>
<button>Cancel</button>
<button>Confirm</button>
</div>
</div>
// Page B:Another pop-up,But the style is slightly different
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
display: 'flex', alignItems: 'center', justifyContent: 'center'
}}>
<div style={{ backgroundColor: 'white', padding: '32px', borderRadius: '12px' }}>
<h2>Edit User</h2>
<input placeholder="Username" />
<input placeholder="Email" />
<button>Save</button>
<button>Cancel</button>
</div>
</div>
Problema: Os estilos da camada da máscara e do contêiner estão duplicados em duas páginas. Se você quiser alterar o tamanho dos cantos arredondados do pop-up, precisará editar 20 páginas.
(2) Solução do modelo combinatório
// General Popup Component(Responsible only for structure and style,Not interested in the content)
function Modal({ isOpen, onClose, title, children }) {
if (!isOpen) return null
return (
<div style={overlayStyle} onClick={onClose}>
<div style={modalStyle} onClick={e => e.stopPropagation()}>
{title && <h2 style={{ margin: '0 0 16px 0' }}>{title}</h2>}
{children} {/* The content is determined by the user. */}
</div>
</div>
)
}
// Page A:Confirm Deletion
<Modal isOpen={showDelete} onClose={() => setShowDelete(false)} title="Confirm Deletion">
<p>Are you sure you want to delete this record??This action cannot be undone.。</p>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button onClick={() => setShowDelete(false)}>Cancel</button>
<button style={{ backgroundColor: '#ff4d4f', color: 'white' }}>Confirm Deletion</button>
</div>
</Modal>
// Page B:Edit User
<Modal isOpen={showEdit} onClose={() => setShowEdit(false)} title="Edit User">
<input placeholder="Username" />
<input placeholder="Email" />
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '16px' }}>
<button>Save</button>
<button onClick={() => setShowEdit(false)}>Cancel</button>
</div>
</Modal>
Vantagens: gerenciamento centralizado dos estilos das janelas pop-up (altere em um lugar, altere em todos); o conteúdo é inteiramente determinado pelo usuário; um único componente modal é reutilizado em 20 páginas.
graph TB
subgraph "Combination Mode"
A[Modal Components] --> B[Overlay]
A --> C[Container modal]
C --> D[Title title]
C --> E[**children slot**]
E --> F[The content is determined by the parent component]
end
style E fill:#1890ff,color:#fff
3. Faixa etária infantil
| Modo de slot | Sintaxe | Casos de uso |
|---|---|---|
| Compartimento único | {children} |
Recipiente simples (cartão, layout) |
| Várias posições | {props.header} {props.footer} |
Layout multirregional (página, caixa de diálogo) |
| renderProps | {props.renderItem(item)} |
Renderização personalizada de listas/tabelas |
| Subcomponente de função | {props.children(data)} |
Propagação de dados + renderização personalizada |
(1) Uso básico
children é um Prop especial que representa o conteúdo entre as tags do componente.
// Component Definition
function Card({ title, children }) {
return (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '8px',
padding: '16px',
marginBottom: '16px'
}}>
{title && <h3 style={{ margin: '0 0 12px 0', color: '#333' }}>{title}</h3>}
<div>{children}</div>
</div>
)
}
// How to Use 1:A single element
<Card title="Announcement">
<p>The system will go live tonight 22:00 Perform maintenance</p>
</Card>
// How to Use 2:Multiple elements
<Card title="User Information">
<p>Name:Alice</p>
<p>Characters:Administrator</p>
<p>Email:alice@example.com</p>
</Card>
// How to Use 3:and even other components
<Card title="Statistical Overview">
<StatCard label="Number of users" value={1234} />
<StatCard label="Number of Orders" value={567} />
</Card>
(2) Modo multislot
Se um componente exigir várias posições de slot, você pode usar props nomeados (como header, footer, main):
function PageLayout({ header, sidebar, children, footer }) {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header Socket */}
<header style={{ backgroundColor: '#001529', color: 'white', padding: '16px' }}>
{header}
</header>
<div style={{ display: 'flex', flex: 1 }}>
{/* Sidebar Slot */}
<aside style={{ width: '240px', backgroundColor: '#f5f5f5', padding: '16px' }}>
{sidebar}
</aside>
{/* Main Content Slot(children) */}
<main style={{ flex: 1, padding: '24px' }}>
{children}
</main>
</div>
{/* Bottom Slot */}
<footer style={{ backgroundColor: '#f0f0f0', padding: '12px', textAlign: 'center' }}>
{footer}
</footer>
</div>
)
}
// Usage
<PageLayout
header={<><h1>Management System</h1><nav>Navigation Menu</nav></>}
sidebar={<><h3>List of Features</h3><ul><li>User Management</li><li>Order Management</li></ul></>}
footer={<p>© 2026 web-tutorial.com</p>}
>
<h2>Welcome back,Administrator</h2>
<p>Today's Orders:128 orders</p>
<p>New Users:23 users</p>
</PageLayout>
▶ Exemplo: Componente de lista genérico
// ============================================
// Example:Generic List Component(Multi-slot + children)
// ============================================
function DataList({ data, renderItem, header, emptyMessage = 'No data available' }) {
return (
<div style={{ border: '1px solid #f0f0f0', borderRadius: '8px' }}>
{/* Header Socket */}
{header && <div style={{ padding: '12px 16px', borderBottom: '1px solid #f0f0f0', backgroundColor: '#fafafa' }}>{header}</div>}
{/* List Contents */}
{data.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: '#999' }}>{emptyMessage}</div>
) : (
data.map((item, index) => (
<div key={item.id} style={{
padding: '12px 16px',
borderBottom: index < data.length - 1 ? '1px solid #f0f0f0' : 'none',
transition: 'background-color 0.2s'
}}
onMouseEnter={e => e.currentTarget.style.backgroundColor = '#fafafa'}
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'transparent'}
>
{renderItem(item)}
</div>
))
)}
{/* children You can add additional action buttons */}
{data.length > 0 && <div style={{ padding: '12px 16px', borderTop: '1px solid #f0f0f0' }}>{data.length} records</div>}
</div>
)
}
// Examples of Use 1:User List
<DataList
data={users}
header={<h3 style={{ margin: 0 }}>User List</h3>}
renderItem={user => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{user.name}</span>
<span style={{ color: '#999' }}>{user.role}</span>
</div>
)}
/>
// Examples of Use 2:Product List
<DataList
data={products}
header={<h3 style={{ margin: 0 }}>Product List <button>Add Item</button></h3>}
renderItem={product => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{product.name}</span>
<span style={{ color: '#ff4d4f' }}>${product.price}</span>
</div>
)}
emptyMessage="No products have been listed yet."
/>
4. Princípios da decomposição de componentes
Bons componentes são como boas funções — têm uma única responsabilidade, são reutilizáveis e fáceis de testar.
| Princípio | Explicação | Contraexemplo |
|---|---|---|
| Responsabilidade Única | Um componente faz apenas uma coisa | Um componente exibe informações do usuário e, ao mesmo tempo, processa exportações de dados |
| Minimizar interfaces | Quanto menos props, melhor (≤5) | Um componente tem 15 props |
| Fluxo de dados descendente | Os dados fluem unidirecionalmente do pai para o filho | Os componentes filhos modificam diretamente o estado global |
| Sem efeitos colaterais | Props idênticos geram resultados idênticos | Modifique o DOM ou chame APIs diretamente dentro do componente |
▶ Exemplo: Antes da divisão vs. Depois da divisão
// ============================================
// Example:Component Breakdown——from "Giant Components"to "Widget"
// ============================================
// ---- Before the split:One component does it all ----
function GiantUserPage({ userId }) {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [editing, setEditing] = React.useState(false)
const [name, setName] = React.useState('')
const [email, setEmail] = React.useState('')
const [posts, setPosts] = React.useState([])
// ... 100 Multiple lines of logic mixed together
return (
<div>
{/* User Information、Edit Form、The list of articles is all crammed together */}
</div>
)
}
// ---- After the split:Each to their own role ----
function UserPage({ userId }) {
return (
<div>
<UserProfile userId={userId} /> {/* Responsible only for displaying user information */}
<UserEditForm userId={userId} /> {/* Responsible only for editing user content */}
<UserPosts userId={userId} /> {/* Responsible only for displaying the list of articles */}
</div>
)
}
// Each subcomponent can be developed independently.、Test、Reuse
function UserProfile({ userId }) { /* ... */ }
function UserEditForm({ userId }) { /* ... */ }
function UserPosts({ userId }) { /* ... */ }
(1) Regras gerais para a divisão
If a component exceeds 150 lines
A function that exceeds 30 lines
One render Return more than 10 JSX Element
Props More than 5
→ Consider a split!
5. Padrão Render Props
Render Props é outro padrão de composição — ele permite que um componente pai controle o que um componente filho renderiza por meio de uma propriedade de função.
// ============================================
// Example:Render Props——Mouse Position Tracker
// ============================================
// Child component:Responsible only for"Track Mouse Position",Don't care"How to display"
function MouseTracker({ render }) {
const [position, setPosition] = React.useState({ x: 0, y: 0 })
function handleMouseMove(e) {
setPosition({ x: e.clientX, y: e.clientY })
}
return (
<div style={{ height: '300px', border: '1px solid #ddd' }} onMouseMove={handleMouseMove}>
{/* Let the parent component determine how to render the mouse position */}
{render(position)}
</div>
)
}
// Parent Component:Decision"How to display"
function App() {
return (
<div>
<h2>Render Props Example</h2>
{/* Usage 1:Display Coordinates */}
<MouseTracker render={({ x, y }) => (
<div style={{ padding: '20px' }}>
<p>Mouse Position:({x}, {y})</p>
</div>
)} />
{/* Usage 2:Display Coordinate Indicator */}
<MouseTracker render={({ x, y }) => (
<div style={{
position: 'absolute', left: x, top: y,
width: '20px', height: '20px',
backgroundColor: '#1890ff', borderRadius: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
}} />
)} />
</div>
)
}
Observação: Desde a introdução dos React Hooks, a maioria dos casos de uso dos Render Props pode ser substituída por hooks personalizados (que são mais concisos). Os Render Props são comumente encontrados em código legado.
6. Composição x Herança
A equipe do React afirma explicitamente: Não use herança. Use composição.
// ❌ Inheritance Patterns(Don't write it that way.)
class BaseButton extends React.Component {
// ...Overriding Methods After Inheritance in a Subclass,React Not recommended
}
class PrimaryButton extends BaseButton { }
// ✅ Combination Mode(The Correct Way to Do It)
function Button({ variant = 'default', children, ...props }) {
const styles = {
primary: { backgroundColor: '#1890ff', color: 'white' },
danger: { backgroundColor: '#ff4d4f', color: 'white' },
default: { backgroundColor: '#f0f0f0', color: '#333' }
}
return <button style={styles[variant]} {...props}>{children}</button>
}
// Through Props Control Variants,rather than through inheritance
<Button variant="primary">Main Buttons</Button>
<Button variant="danger">Danger Button</Button>
<Button variant="default">Default Button</Button>
| Dimensão | Combinação (recomendada) | Herança (não recomendada) |
|---|---|---|
| Flexibilidade | Alta — Combinação livre por meio de props e elementos filhos | Baixa — Cadeia de herança fixa |
| Testabilidade | Alta — Cada componente pode ser testado de forma independente | Baixa — Depende da implementação da classe pai |
| Segurança de tipos | Boa — os props são explicitamente tipados | Ruim — a sobrescrita de métodos está sujeita a erros |
7. Exemplo completo: um sistema reutilizável de cartões para painéis de controle
// ============================================
// Complete Example:Dashboard Card System
// Features:Combination Mode + children + Props Integrated Use of
// ============================================
// ---- 1. Basic Components:Card Holder ----
function DashboardCard({ title, icon, action, children, width = '300px' }) {
return (
<div style={{
border: '1px solid #f0f0f0',
borderRadius: '8px',
width,
backgroundColor: 'white',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)'
}}>
{/* Card Header */}
<div style={{
display: 'flex', justifyContent: 'space-between',
alignItems: 'center', padding: '16px',
borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{icon && <span>{icon}</span>}
<h3 style={{ margin: 0, fontSize: '16px' }}>{title}</h3>
</div>
{action && <div>{action}</div>}
</div>
{/* Card Content Slot */}
<div style={{ padding: '16px' }}>
{children}
</div>
</div>
)
}
// ---- 2. Statistics Card ----
function StatCard({ label, value, trend, color = '#1890ff' }) {
return (
<div style={{ textAlign: 'center', padding: '8px 0' }}>
<p style={{ color: '#666', fontSize: '14px', margin: '0 0 8px 0' }}>{label}</p>
<p style={{ fontSize: '32px', fontWeight: 'bold', color, margin: '0 0 8px 0' }}>{value}</p>
{trend !== undefined && (
<p style={{
color: trend >= 0 ? '#52c41a' : '#ff4d4f',
fontSize: '14px', margin: 0
}}>
{trend >= 0 ? '↑' : '↓'} {Math.abs(trend)}%
</p>
)}
</div>
)
}
// ---- 3. List Cards ----
function ListCard({ title, icon, items, renderItem, action, emptyText = 'No data available' }) {
return (
<DashboardCard title={title} icon={icon} action={action}>
{items.length === 0 ? (
<p style={{ textAlign: 'center', color: '#999', padding: '20px' }}>{emptyText}</p>
) : (
items.map((item, index) => (
<div key={item.id} style={{
padding: '8px 0',
borderBottom: index < items.length - 1 ? '1px solid #f5f5f5' : 'none'
}}>
{renderItem(item)}
</div>
))
)}
</DashboardCard>
)
}
// ---- 4. Usage:Complete Dashboard ----
function Dashboard() {
const stats = [
{ id: 'users', label: 'Total Users', value: '12,846', trend: 12 },
{ id: 'orders', label: 'Today's Orders', value: '328', trend: -3 },
{ id: 'revenue', label: 'Monthly Income', value: '$89,200', trend: 25 },
{ id: 'visits', label: 'Visits Today', value: '4,621', trend: 8 }
]
const recentOrders = [
{ id: 1, customer: 'Alice', product: 'React Tutorial', amount: 89, status: 'Completed' },
{ id: 2, customer: 'Bob', product: 'TypeScript Getting Started', amount: 59, status: 'Processing...' },
{ id: 3, customer: 'Charlie', product: 'Node.js Real-World Experience', amount: 129, status: 'Completed' }
]
return (
<div>
<h2>📊 Management Dashboard</h2>
{/* Statistical Card Rows(Grid Layout) */}
<div style={{ display: 'flex', gap: '16px', marginBottom: '24px', flexWrap: 'wrap' }}>
<DashboardCard title="Data Overview" icon="📈" width="100%">
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px' }}>
{stats.map(s => (
<StatCard key={s.id} {...s} />
))}
</div>
</DashboardCard>
</div>
{/* List of Recent Orders */}
<div style={{ display: 'flex', gap: '16px' }}>
<ListCard
title="Recent Orders"
icon="📋"
items={recentOrders}
action={<button style={btnStyle}>View All</button>}
renderItem={order => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<p style={{ margin: 0, fontWeight: 'bold' }}>{order.customer}</p>
<p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '13px' }}>{order.product}</p>
</div>
<div style={{ textAlign: 'right' }}>
<p style={{ margin: 0, color: '#ff4d4f' }}>${order.amount}</p>
<span style={{
fontSize: '12px', padding: '2px 6px', borderRadius: '4px',
backgroundColor: order.status === 'Completed' ? '#f6ffed' : '#fff7e6',
color: order.status === 'Completed' ? '#52c41a' : '#faad14'
}}>
{order.status}
</span>
</div>
</div>
)}
/>
{/* Custom Cards:Use directly children */}
<DashboardCard title="Quick Actions" icon="⚡" width="300px">
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button style={actionBtnStyle}>📝 Create a New Order</button>
<button style={actionBtnStyle}>👤 Add User</button>
<button style={actionBtnStyle}>📊 Generate Reports</button>
<button style={actionBtnStyle}>⚙️ System Settings</button>
</div>
</DashboardCard>
</div>
</div>
)
}
const btnStyle = {
padding: '4px 12px', border: '1px solid #d9d9d9',
borderRadius: '4px', backgroundColor: 'white',
cursor: 'pointer', fontSize: '12px'
}
const actionBtnStyle = {
width: '100%', padding: '10px', border: 'none',
borderRadius: '4px', backgroundColor: '#f5f5f5',
cursor: 'pointer', textAlign: 'left', fontSize: '14px',
transition: 'background-color 0.2s'
}
// Hover effects are enabled via onMouseEnter/Leave Add,Omitted here
Resultado esperado: Uma página de painel completa, apresentando um cartão de visão geral dos dados (com 4 métricas) na parte superior, uma lista de pedidos recentes no canto inferior esquerdo e um conjunto de botões de ação rápida no canto inferior direito. Todos os cartões são criados utilizando o padrão de composição.
▶ Exemplo 3: Render Props — Tabelas classificáveis
function SortableTable({ data, columns, renderCell }) {
const [sortKey, setSortKey] = useState(null)
const [sortDir, setSortDir] = useState('asc')
const sorted = useMemo(() => {
if (!sortKey) return data
return [...data].sort((a, b) => {
const diff = a[sortKey] > b[sortKey] ? 1 : a[sortKey] < b[sortKey] ? -1 : 0
return sortDir === 'asc' ? diff : -diff
})
}, [data, sortKey, sortDir])
function handleSort(key) {
if (sortKey === key) setSortDir(d => d === 'asc' ? 'desc' : 'asc')
else { setSortKey(key); setSortDir('asc') }
}
return (
<table border={1} cellPadding={8} style={{ borderCollapse: 'collapse', width: '100%' }}>
<thead>
<tr>
{columns.map(col => (
<th key={col.key} onClick={() => handleSort(col.key)} style={{ cursor: 'pointer' }}>
{col.label} {sortKey === col.key ? (sortDir === 'asc' ? '↑' : '↓') : ''}
</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((row, i) => (
<tr key={i}>{columns.map(col => <td key={col.key}>{renderCell(row, col.key)}</td>)}</tr>
))}
</tbody>
</table>
)
}
function App() {
const users = [
{ name: 'Alice', score: 92, role: 'Admin' },
{ name: 'Bob', score: 85, role: 'Editor' },
{ name: 'Charlie', score: 78, role: 'Viewer' },
]
const cols = [{ key: 'name', label: 'Name' }, { key: 'score', label: 'Score' }, { key: 'role', label: 'Role' }]
return (
<div style={{ maxWidth: 500, margin: '0 auto' }}>
<h3>Sortable Users</h3>
<SortableTable data={users} columns={cols}
renderCell={(row, key) => key === 'score' ? `${row[key]} pts` : row[key]} />
</div>
)
}
▶ Exemplo 4: Combinações de layouts — Layouts multirregionais
function AppLayout({ sidebar, header, content, footer }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
{sidebar && <aside style={{ width: 200, background: '#f5f5f5', padding: 16 }}>{sidebar}</aside>}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
{header && <header style={{ padding: '12px 16px', background: '#1890ff', color: 'white' }}>{header}</header>}
<main style={{ flex: 1, padding: 16 }}>{content}</main>
{footer && <footer style={{ padding: '8px 16px', background: '#fafafa', fontSize: 12, color: '#999' }}>{footer}</footer>}
</div>
</div>
)
}
function Dashboard() {
return (
<AppLayout
sidebar={<nav><p>Menu</p><ul><li>Home</li><li>Settings</li></ul></nav>}
header={<h2 style={{ margin: 0 }}>Dashboard</h2>}
content={<p>Welcome back, Alice! You have 5 new tasks.</p>}
footer={<span>© 2026 SaaS App</span>}
/>
)
}
▶ Exemplo 5: Composição x Herança — Componente pop-up
function Modal({ isOpen, onClose, title, children, footer }) {
if (!isOpen) return null
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}
onClick={onClose}>
<div style={{ background: 'white', borderRadius: 8, padding: 24, minWidth: 320, maxWidth: 500 }}
onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h3 style={{ margin: 0 }}>{title}</h3>
<button onClick={onClose} style={{ border: 'none', background: 'none', cursor: 'pointer', fontSize: 18 }}>✕</button>
</div>
<div style={{ marginBottom: 16 }}>{children}</div>
{footer && <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>{footer}</div>}
</div>
</div>
)
}
function ConfirmModal({ isOpen, onConfirm, onCancel, message }) {
return (
<Modal isOpen={isOpen} onClose={onCancel} title="Confirm"
footer={<>
<button onClick={onCancel} style={{ padding: '6px 16px', cursor: 'pointer' }}>Cancel</button>
<button onClick={onConfirm} style={{ padding: '6px 16px', background: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Confirm</button>
</>}>
<p>{message}</p>
</Modal>
)
}
function FormModal({ isOpen, onClose }) {
const [value, setValue] = useState('')
return (
<Modal isOpen={isOpen} onClose={onClose} title="New Item"
footer={<button onClick={() => { alert(value); onClose() }} style={{ padding: '6px 16px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Submit</button>}>
<input value={value} onChange={e => setValue(e.target.value)} placeholder="Enter name" style={{ width: '100%', padding: 8, borderRadius: 4 }} />
</Modal>
)
}
❓ Perguntas Frequentes
P: Quando se deve usar
childrene quando se deve usarProps? R:childrené usado para componentes contêineres com “áreas de conteúdo variável” (como modais, cartões e layouts).Propsé usado para componentes com “conteúdo configurável” (como cores de botões e texto de título). Resumindo: usechildrenpara elementos estruturais ePropspara configuração.
P: Como escolho entre Render Props e Hooks? R: Para código novo, priorize Hooks personalizados. Os Render Props são adequados para cenários que envolvem tanto lógica de estado quanto estrutura da interface do usuário (como o MouseTracker, que requer um DOM provisório). Os Hooks são adequados para cenários que envolvem apenas lógica de estado (como o useMousePosition, que retorna apenas coordenadas e não renderiza nenhuma interface do usuário). O React recomenda oficialmente os Hooks.
P: O que devo fazer se houver muitos props? R: Isso indica que o componente tem responsabilidades demais e precisa ser dividido. Recomendação: agrupe os props de forma que cada grupo corresponda às responsabilidades de um componente filho. Por exemplo, se um componente
UserFormtiver 12 props, divida-o em três componentes filhos:UserBasicInfo(nome, e-mail, número de telefone),UserAddress(província, cidade, distrito, endereço detalhado) eUserSettings(função, permissões).
P: Qual deve ser o tamanho ideal de um componente? R: Não há regras rígidas e definitivas, mas aqui estão algumas orientações: ① Um arquivo de componente não deve exceder 150 linhas; ② Um único componente não deve ter mais de 5 props; ③ Um componente deve fazer apenas “uma coisa” (Princípio da Responsabilidade Única); ④ Se um componente contiver várias seções
if/elseque renderizam conteúdos diferentes, considere dividi-lo em vários componentes filhos. É melhor dividi-lo em mais componentes do que em poucos — é mais fácil combiná-los do que separá-los.
P: O que é melhor, Render Props ou Hooks? R: Os Hooks são a melhor opção. Os Render Props eram o padrão de reutilização usado no React antes da versão 16.7, exigindo funções de callback aninhadas, o que levava ao “callback hell”. Os Hooks permitem reutilizar lógica com apenas
useXxx()uma única linha de código, resultando em um código mais simples e legível. Os Render Props só apresentam vantagem quando a lógica reutilizada precisa “renderizar uma parte da interface do usuário” (como quando um componente de dica de ferramenta precisa saber a posição de seus elementos filhos).
📖 Resumo
- A composição é melhor do que a herança — No React, usamos a composição 100% das vezes e nunca usamos a herança.
childrené um prop especial que implementa o padrão “slot”, permitindo que os componentes sejam aninhados de forma flexível- O modo multi-slot pode ser usado com props nomeadas (header/footer/sidebar) para implementar layouts complexos
- A decomposição dos componentes segue o Princípio da Responsabilidade Única: um componente faz apenas uma coisa
- O
Render Propspassa funções de renderização; os hooks são uma alternativa melhor.
📝 Exercícios
- Problema básico (Dificuldade ⭐): Crie um componente
Badgeque aceitecount(um número) echildren(conteúdo) e exiba um número em sobrescrito vermelho no canto superior direito. - Exercício avançado (Dificuldade ⭐⭐): Crie um componente
Tabse use o Padrão Composto para implementar a troca de rótulos:<Tabs><TabPanel label="Tab1">inside content 1</TabPanel><TabPanel label="Tab2">inside content 2</TabPanel></Tabs>. - Desafio (Dificuldade: ⭐⭐⭐): Crie um componente
Tableusando o padrão Render Props:<Table data={users} columns={[{ title: 'Name', render: u => u.name }, { title: 'Age', render: u => u.age }]} />, e inclua suporte para ordenação e destaque em zebra.