kb.erickguedes.com
React: Construindo Interfaces Modernas

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?"