Comunicação entre Componentes
Aula 4 de 8
Props Drilling
Passar props por vários níveis de componentes:
interface Usuario {
nome: string;
email: string;
}
function App() {
const [usuario] = useState<Usuario>({
nome: "Alice",
email: "[email protected]",
});
return <Pagina usuario={usuario} />;
}
function Pagina({ usuario }: { usuario: Usuario }) {
return (
<div>
<Cabecalho usuario={usuario} />
<Conteudo />
</div>
);
}
function Cabecalho({ usuario }: { usuario: Usuario }) {
return (
<header>
<Avatar usuario={usuario} />
</header>
);
}
function Avatar({ usuario }: { usuario: Usuario }) {
return <span>{usuario.nome[0]}</span>;
}
Problema: componentes intermediários (Pagina, Cabecalho) recebem props que não usam.
Composição (children, slots)
Elimina props drilling delegando a renderização:
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);
}
// Uso — sem prop drilling
function App() {
return (
<Layout
header={<Cabecalho />}
sidebar={<Sidebar />}
>
<Conteudo />
</Layout>
);
}
Render Props Pattern
Componente recebe uma função que renderiza JSX:
interface MouseTrackerProps {
render: (pos: { x: number; y: number }) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);
return <div>{render(pos)}</div>;
}
// Uso
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<p>Mouse position: {x}, {y}</p>
)}
/>
);
}
Compound Components
Componentes que trabalham juntos compartilhando estado implícito:
import { createContext, useContext, useState, type ReactNode } from "react";
// Context interno
interface AccordionContextType {
aberto: string | null;
toggle: (id: string) => void;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
// Componente pai
function Accordion({ children }: { children: ReactNode }) {
const [aberto, setAberto] = useState<string | null>(null);
const toggle = (id: string) =>
setAberto((prev) => (prev === id ? null : id));
return (
<AccordionContext.Provider value={{ aberto, toggle }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
// Subcomponente Item
function Item({ id, titulo, children }: {
id: string;
titulo: string;
children: ReactNode;
}) {
const ctx = useContext(AccordionContext)!;
return (
<div className="accordion-item">
<button onClick={() => ctx.toggle(id)}>{titulo}</button>
{ctx.aberto === id && <div className="accordion-content">{children}</div>}
</div>
);
}
// Adicionar Item como propriedade do Accordion
Accordion.Item = Item;
// Uso
function App() {
return (
<Accordion>
<Accordion.Item id="1" titulo="Seção 1">
Conteúdo da seção 1
</Accordion.Item>
<Accordion.Item id="2" titulo="Seção 2">
Conteúdo da seção 2
</Accordion.Item>
</Accordion>
);
}
Lifting State Up
Elevar o estado ao ancestral comum mais próximo:
function App() {
const [celsius, setCelsius] = useState(0);
return (
<div>
<InputTemperatura
label="Celsius"
valor={celsius}
onChange={setCelsius}
/>
<KelvinDisplay celsius={celsius} />
<FahrenheitDisplay celsius={celsius} />
</div>
);
}
interface InputTemperaturaProps {
label: string;
valor: number;
onChange: (v: number) => void;
}
function InputTemperatura({ label, valor, onChange }: InputTemperaturaProps) {
return (
<div>
<label>{label}</label>
<input
type="number"
value={valor}
onChange={(e) => onChange(Number(e.target.value))}
/>
</div>
);
}
function KelvinDisplay({ celsius }: { celsius: number }) {
return <p>Kelvin: {celsius + 273.15}</p>;
}
function FahrenheitDisplay({ celsius }: { celsius: number }) {
return <p>Fahrenheit: {(celsius * 9 / 5) + 32}</p>;
}
Colocation
Manter estado e lógica perto de onde são usados, em vez de num store global:
// ✅ Colocado: estado no componente que precisa dele
function ListaComFiltro() {
const [filtro, setFiltro] = useState("");
// fetch local, não no store global
const { dados } = useFetch<Item[]>(`/api/items?q=${filtro}`);
return (
<div>
<input value={filtro} onChange={(e) => setFiltro(e.target.value)} />
<ItemsList items={dados ?? []} />
</div>
);
}
// ❌ Evitar: colocar tudo no store global sem necessidade
Lab: Sistema de Abas (Tabs) com Compound Components
interface TabsContextType {
ativa: string;
setAtiva: (id: string) => void;
}
const TabsContext = createContext<TabsContextType | null>(null);
function Tabs({ children, padrao }: {
children: ReactNode;
padrao: string;
}) {
const [ativa, setAtiva] = useState(padrao);
return (
<TabsContext.Provider value={{ ativa, setAtiva }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Lista({ children }: { children: ReactNode }) {
return <div className="tabs-lista" role="tablist">{children}</div>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
return (
<button
role="tab"
aria-selected={ctx.ativa === id}
onClick={() => ctx.setAtiva(id)}
className={ctx.ativa === id ? "ativa" : ""}
>
{children}
</button>
);
}
function Painel({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext)!;
if (ctx.ativa !== id) return null;
return <div role="tabpanel">{children}</div>;
}
Tabs.Lista = Lista;
Tabs.Tab = Tab;
Tabs.Painel = Painel;
// Uso
function App() {
return (
<Tabs padrao="ts">
<Tabs.Lista>
<Tabs.Tab id="ts">TypeScript</Tabs.Tab>
<Tabs.Tab id="react">React</Tabs.Tab>
</Tabs.Lista>
<Tabs.Painel id="ts">TypeScript é um superset do JS</Tabs.Painel>
<Tabs.Painel id="react">React é uma biblioteca de UI</Tabs.Painel>
</Tabs>
);
}
Composição vence props drilling. Antes de adicionar um store global (Zustand/Redux), pergunte: "Esse estado precisa estar aqui ou pode ficar mais perto de quem usa?"