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



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:

JSX
// 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>
▶ Experimente

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

JSX
// 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>
▶ Experimente

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.

100%
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.

JSX
// 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>
▶ Experimente

(2) Modo multislot

Se um componente exigir várias posições de slot, você pode usar props nomeados (como header, footer, main):

JSX
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>
▶ Experimente

▶ Exemplo: Componente de lista genérico

JSX 📖 Somente leitura
// ============================================
// 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."
/>
48 linhas de lógica (limite de 40, somente leitura)

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

JSX
// ============================================
// 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 }) { /* ... */ }
▶ Experimente

(1) Regras gerais para a divisão

BASH
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.

JSX
// ============================================
// 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>
  )
}
▶ Experimente

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.

JSX
// ❌ 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>
▶ Experimente
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

JSX
// ============================================
// 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

JSX 📖 Somente leitura
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>
  )
}
48 linhas de lógica (limite de 40, somente leitura)

▶ Exemplo 4: Combinações de layouts — Layouts multirregionais

JSX
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>}
    />
  )
}
▶ Experimente

▶ Exemplo 5: Composição x Herança — Componente pop-up

JSX
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>
  )
}
▶ Experimente

❓ Perguntas Frequentes

P: Quando se deve usar children e quando se deve usar Props? 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: use children para elementos estruturais e Props para 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 UserForm tiver 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) e UserSettings (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/else que 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


📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Crie um componente Badge que aceite count (um número) e children (conteúdo) e exiba um número em sobrescrito vermelho no canto superior direito.
  2. Exercício avançado (Dificuldade ⭐⭐): Crie um componente Tabs e 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>.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um componente Table usando 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%