Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Renderização condicional no React: if, && e ternário

Quatro formas de mostrar e esconder pedaços da tela no React, o bug do && com zero e quando o early return deixa o componente mais legível.

Rodolfo Mori12 min de leitura

Você abre uma tela React e encontra “carregando” junto de “nenhum resultado”. Em outra página, uma fila vazia deixa um 0 perdido. Os dois bugs nascem da mesma decisão: qual pedaço da interface deve existir em cada momento?

Aqui você vai entender o motivo de cada escolha antes da sintaxe. Renderizar é transformar dados em elementos da interface; renderização condicional é escolher quais serão produzidos. O React faz isso com JavaScript — if, ternário e && — dentro ou em volta do JSX, a sintaxe que descreve a tela. A gente começa com um aviso e termina nos quatro estados de uma tela real: carregando, erro, vazio e pronto.

Os exemplos usam o painel de uma clínica veterinária. Cada saída é o HTML que o React 19.2.8 realmente produziu com renderToStaticMarkup, do react-dom/server, no Node 24.16.0. A função transforma o componente em uma string de HTML, então dá para comparar decisão e resultado concreto.

O que é renderização condicional no React?

É escolher o trecho que o React produz a partir dos dados atuais. Essa decisão mantém a tela coerente, sem mostrar avisos e resultados incompatíveis juntos.

Imagine o painel como o espaço de uma placa na recepção: os dados são a informação recebida, a condição é a pergunta, o JSX retornado é a placa escolhida e o HTML é o que o cliente vê. Carregamento, erro e agenda vazia pedem placas diferentes.

A analogia termina aí: o React não troca objetos físicos. Quando props ou estado mudam, ele executa o componente de novo, compara a nova descrição com a anterior e então atualiza o DOM, a árvore de elementos da página. É esse fluxo que as condicionais controlam.

Comece pela menor decisão: o aviso produz um parágrafo ou nada. Como ela vale para o componente inteiro, pode vir antes do JSX, com if e return null.

jsx
import { renderToStaticMarkup } from 'react-dom/server';

function AvisoDeVacina({ pet }) {
  if (pet.vacinaEmDia) return null;

  return <p className="alerta">{pet.nome} está com a vacina atrasada.</p>;
}

const pets = [
  { nome: 'Nina', vacinaEmDia: true },
  { nome: 'Thor', vacinaEmDia: false },
];

console.log(
  renderToStaticMarkup(
    <section>
      {pets.map((pet) => (
        <AvisoDeVacina key={pet.nome} pet={pet} />
      ))}
    </section>,
  ),
);
<section><p class="alerta">Thor está com a vacina atrasada.</p></section>

A função recebe pet por uma prop, um dado entregue ao componente. O if verifica a vacina; depois, map percorre os pets e key identifica cada item.

A Nina sumiu do HTML por completo — nem uma tag vazia sobrou. return null é a forma de dizer “não tenho nada para desenhar agora”. Thor chega ao segundo return e produz o parágrafo.

Repare no ganho de leitura: o return de baixo trata de um caso só. Quem abre o arquivo lê “se a vacina está em dia, não tem aviso” na primeira linha e já sabe o resto. Esse retorno antecipado recebe o nome técnico early return: a função termina cedo quando encontra um caso que já pode responder. Essa é a razão de ele ser a primeira opção, não a última.

Quando usar o operador ternário no JSX?

Use o operador ternário quando existem duas respostas visuais curtas para a mesma condição. Ele tem três partes: primeiro a pergunta, depois o valor usado quando ela é verdadeira e, por fim, o valor usado quando é falsa. Como o ternário é uma expressão — um trecho que produz um valor — ele cabe entre chaves no JSX.

jsx
function Situacao({ consulta }) {
  return (
    <span className="situacao">
      {consulta.confirmada ? 'Confirmada' : 'Aguardando confirmação'}
    </span>
  );
}

console.log(renderToStaticMarkup(<Situacao consulta={{ confirmada: true }} />));
console.log(renderToStaticMarkup(<Situacao consulta={{ confirmada: false }} />));
<span class="situacao">Confirmada</span> <span class="situacao">Aguardando confirmação</span>

Na primeira chamada a condição é verdadeira; na segunda, falsa. O mesmo span continua na tela, mas o texto muda de lado no ternário.

O problema aparece quando a decisão deixa de ter apenas dois lados. A consulta pode estar confirmada, cancelada, em atendimento ou aguardando — e o ternário encadeado obriga você a contar sinais para descobrir o resultado:

jsx
function SeloSopa({ consulta }) {
  return (
    <span className="selo">
      {consulta.situacao === 'confirmada'
        ? 'Confirmada'
        : consulta.situacao === 'cancelada'
          ? 'Cancelada'
          : consulta.situacao === 'em_atendimento'
            ? 'Em atendimento'
            : 'Aguardando'}
    </span>
  );
}

Funciona, e ninguém consegue ler. Com mais de duas opções, tire a decisão de dentro do JSX e transforme ela em dado. Um objeto de consulta com valor padrão faz o mesmo trabalho:

jsx
const SELOS = {
  confirmada: 'Confirmada',
  cancelada: 'Cancelada',
  em_atendimento: 'Em atendimento',
};

function Selo({ consulta }) {
  return <span className="selo">{SELOS[consulta.situacao] ?? 'Aguardando'}</span>;
}

Rodei os dois lado a lado com as quatro situações e comparei o HTML gerado string por string:

confirmada igual <span class="selo">Confirmada</span> cancelada igual <span class="selo">Cancelada</span> em_atendimento igual <span class="selo">Em atendimento</span> remarcada igual <span class="selo">Aguardando</span>

Mesmo resultado, incluindo o caso remarcada, que não estava previsto em lugar nenhum: no encadeamento ele cai no último else, no objeto ele cai no ??. A diferença é que a segunda versão cabe numa linha, e a lista de selos pode ser testada sem montar componente nenhum.

Por que o && mostra zero no React?

Porque o && devolve um dos operandos, e não obrigatoriamente um booleano. Se o primeiro operando for 0, o JavaScript para ali, entrega esse número ao JSX e o React o mostra na tela.

Quando só existe o caso positivo, o && é mais curto que um ternário com null: à esquerda fica a condição; à direita, o elemento. Essa concisão abre espaço para o bug central desta lição.

jsx
function FilaDeEspera({ pets }) {
  return (
    <div className="fila">
      {pets.length && <p>{pets.length} pet aguardando atendimento</p>}
    </div>
  );
}

console.log(renderToStaticMarkup(<FilaDeEspera pets={[{ nome: 'Nina' }, { nome: 'Thor' }]} />));
console.log(renderToStaticMarkup(<FilaDeEspera pets={[]} />));

Com dois pets na fila, funciona. Com a fila vazia, não:

<div class="fila"><p>2 pet aguardando atendimento</p></div> <div class="fila">0</div>

Esse 0 solto dentro da div é literalmente o que aparece na tela da recepcionista. E não é bug do React: o && do JavaScript não devolve true ou false, devolve um dos dois operandos. Com a fila vazia, pets.length vale 0, o && para ali e entrega o 0 ao JSX. O React recebe um número e faz o que faria com qualquer número: imprime.

Três correções, todas com o mesmo resultado — a div sai vazia:

jsx
// cada uma no lugar da linha de dentro da <div className="fila">
{pets.length > 0 && <p>{pets.length} pet aguardando</p>}
{Boolean(pets.length) && <p>{pets.length} pet aguardando</p>}
{pets.length ? <p>{pets.length} pet aguardando</p> : null}
<div class="fila"></div> <div class="fila"></div> <div class="fila"></div>

A primeira deixa a intenção explícita: pets.length > 0 diz em voz alta o que você quer, e não depende de ninguém lembrar de que 0 é falsy mas ainda assim renderizável.

Quais valores o React renderiza no JSX?

O React mostra números, incluindo 0 e NaN, mas omite string vazia, false, null e undefined. Vale conhecer a lista inteira porque o zero não está sozinho. Coloquei os oito valores falsos do JavaScript dentro de uma div, um por vez, e imprimi o HTML resultante com JSON.stringify, para a string vazia ficar visível:

jsx
const valores = [
  ['0', 0],
  ['-0', -0],
  ['0n', 0n],
  ["''", ''],
  ['false', false],
  ['null', null],
  ['undefined', undefined],
  ['NaN', NaN],
];

for (const [rotulo, valor] of valores) {
  console.log(rotulo.padEnd(10), JSON.stringify(renderToStaticMarkup(<div>{valor}</div>)));
}
0 "<div>0</div>" -0 "<div>0</div>" 0n "<div>0</div>" '' "<div></div>" false "<div></div>" null "<div></div>" undefined "<div></div>" Received NaN for the `children` attribute. If this is expected, cast the value to a string. NaN "<div>NaN</div>"

A linha sem aspas no meio da lista é um aviso do próprio React, impresso antes do resultado do NaN — ele desconfia do valor, avisa, e renderiza assim mesmo.

valor sai no HTML risco no &&
0 0 alto — length, contador, preço
NaN NaN (com aviso no console) alto — conta que deu errado
-0 0 baixo — só em conta que troca de sinal
0n 0 baixo — só em projeto que usa BigInt
'' nada nenhum
false nada nenhum
null nada nenhum
undefined nada nenhum

Metade vaza, metade some. Tudo que é zero — 0, o -0 de uma subtração e o BigInt 0n — vira o texto 0; o NaN vira NaN. Só '', false, null e undefined o React descarta em silêncio. Na prática quem pega você é o 0 de um length, mas a regra que evita os quatro é a mesma: todo valor à esquerda de um && no JSX precisa ser booleano de verdade.

E existe um valor que não vaza: ele derruba. Se o que sobrar do && for um objeto — o caso clássico é consulta.tutor virar { nome, telefone } depois de uma mudança na API — o React lança:

jsx
const consulta = { pet: 'Nina', tutor: { nome: 'Marina', telefone: '11 90000-0000' } };

function CartaoDoTutor({ consulta }) {
  return <article>{consulta.tutor && consulta.tutor}</article>;
}

console.log(renderToStaticMarkup(<CartaoDoTutor consulta={consulta} />));
/private/tmp/vet/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6474 throw Error( ^

Error: Objects are not valid as a React child (found: object with keys {nome, telefone}). If you meant to render a collection of children, use an array instead. at retryNode (/private/tmp/vet/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6474:17) at renderNodeDestructive (/private/tmp/vet/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6367:11) at renderNode (/private/tmp/vet/node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js:6905:18)

Node.js v24.16.0

A mensagem entrega o diagnóstico de graça: object with keys {nome, telefone}. É só procurar no componente qual expressão devolve um objeto com esses campos — aqui, trocar por consulta.tutor.nome.

Resumo até aqui: early return encerra um caso, ternário escolhe entre duas respostas curtas e && mostra o lado positivo com uma condição booleana. Agora a escolha também passa a afetar estado e dados de formulário.

Qual é a diferença entre return null e esconder com CSS?

return null remove o elemento; CSS pode tirá-lo da vista sem removê-lo do HTML. Por isso as opções afetam formulário, estado e montagem de maneiras diferentes:

jsx
function CampoRetorno({ mostrar }) {
  if (!mostrar) return null;
  return <input name="dataRetorno" defaultValue="2026-06-01" />;
}

function CampoRetornoCss({ mostrar }) {
  return (
    <input
      name="dataRetorno"
      defaultValue="2026-06-01"
      style={{ display: mostrar ? 'block' : 'none' }}
    />
  );
}

console.log('null :', renderToStaticMarkup(<form><CampoRetorno mostrar={false} /></form>));
console.log('css  :', renderToStaticMarkup(<form><CampoRetornoCss mostrar={false} /></form>));
null : <form></form> css : <form><input style="display:none" name="dataRetorno" value="2026-06-01"/></form>

Na versão CSS o input continua lá, com nome e valor. E campo escondido por CSS continua sendo enviado no submit: display: none esconde da vista, não do formulário. Se o dado não pode ir para a API quando o campo está fechado, null é a única opção correta.

A diferença mais importante, porém, é o estado. Um componente removido do JSX é desmontado, e desmontar apaga o useState dele. Montei as duas versões de uma ficha com um textarea de observações numa página de verdade (jsdom mais createRoot), digitei, escondi e mostrei de novo:

jsx
import { useState } from 'react';

function Observacoes() {
  const [texto, setTexto] = useState('');
  return (
    <textarea name="observacoes" value={texto} onChange={(e) => setTexto(e.target.value)} />
  );
}

function Ficha({ modo, aberto }) {
  return (
    <div>
      {modo === 'null'
        ? aberto && <Observacoes />
        : <div style={{ display: aberto ? 'block' : 'none' }}><Observacoes /></div>}
    </div>
  );
}
null | digitado : "Pet chegou mancando da pata traseira" null | ao voltar: "" css | digitado : "Pet chegou mancando da pata traseira" css | ao voltar: "Pet chegou mancando da pata traseira"

O que a recepcionista digitou some na versão com null e sobrevive na versão com CSS. Isso acontece porque desmontar é retirar o componente da árvore; ao ser montado novamente, ele começa com um novo estado. Nenhuma das duas opções está errada — o que existe é uma escolha:

  • null é o padrão. Menos HTML, estado limpo a cada abertura.
  • CSS só quando você quer mesmo preservar o que a pessoa digitou, ou quando o componente é caro de montar e a alternância é frequente.

Como tratar carregamento, erro, vazio e sucesso no React?

Verifique nesta ordem: carregando, erro, vazio e sucesso. Agora a decisão chega a uma tela de produto: carregando acompanha a requisição, erro guarda a falha e consultas traz a lista. Early returns separam os três primeiros casos e deixam o último return para a agenda pronta:

jsx
function AgendaDoDia({ carregando, erro, consultas }) {
  if (carregando) return <p className="carregando">Carregando a agenda…</p>;
  if (erro) return <p className="erro">Não deu para carregar: {erro}</p>;
  if (consultas.length === 0) return <p className="vazio">Nenhuma consulta marcada para hoje.</p>;

  return (
    <ul className="agenda">
      {consultas.map((c) => (
        <li key={c.id}>
          {c.hora}{c.pet} ({c.tutor})
        </li>
      ))}
    </ul>
  );
}

Rodando os quatro cenários — com erro: 'servidor fora do ar' e duas consultas na lista quando ela vem cheia — este é o HTML de cada um:

carregando <p class="carregando">Carregando a agenda…</p> erro <p class="erro">Não deu para carregar: servidor fora do ar</p> vazio <p class="vazio">Nenhuma consulta marcada para hoje.</p> pronto <ul class="agenda"><li>09:00 — Nina (Marina)</li><li>10:30 — Thor (Caio)</li></ul>

A ordem dos if é a regra de negócio, e trocá-la quebra a tela sem gerar erro nenhum. Se a verificação de lista vazia subir para o topo, os outros três estados param de existir:

jsx
function AgendaOrdemErrada({ carregando, erro, consultas }) {
  if (consultas.length === 0) return <p className="vazio">Nenhuma consulta marcada para hoje.</p>;
  if (carregando) return <p className="carregando">Carregando a agenda…</p>;
  if (erro) return <p className="erro">Não deu para carregar: {erro}</p>;
  return <ul className="agenda">…</ul>;
}
ainda carregando: <p class="vazio">Nenhuma consulta marcada para hoje.</p> deu erro : <p class="vazio">Nenhuma consulta marcada para hoje.</p>

Enquanto a requisição está no ar, consultas ainda é [] — e a tela mente dizendo que não há consultas. Quando o servidor cai, ela mente de novo, e o usuário nunca fica sabendo do erro. Guarde a ordem: carregando, erro, vazio, pronto, sempre nessa sequência. Ela reaparece inteira quando você for consumir uma API no React.

Como mudar atributos com renderização condicional?

Passe a condição no atributo quando o elemento continua na tela e só muda de aparência ou comportamento: classe, disabled ou aria-*, que comunica o estado do controle às tecnologias assistivas.

jsx
function BotaoConfirmar({ consulta }) {
  return (
    <button
      className={consulta.atrasada ? 'botao botao-atrasado' : 'botao'}
      disabled={consulta.situacao === 'cancelada'}
      aria-busy={consulta.enviando}
    >
      Confirmar consulta
    </button>
  );
}

console.log(
  renderToStaticMarkup(
    <BotaoConfirmar consulta={{ atrasada: true, situacao: 'aguardando', enviando: true }} />,
  ),
);
console.log(
  renderToStaticMarkup(
    <BotaoConfirmar consulta={{ atrasada: false, situacao: 'cancelada', enviando: false }} />,
  ),
);
<button class="botao botao-atrasado" aria-busy="true">Confirmar consulta</button> <button class="botao" disabled="" aria-busy="false">Confirmar consulta</button>

Duas coisas para reparar na saída. Na primeira linha o disabled={false} sumiu do HTML, e na segunda ele virou disabled="". Em atributo booleano do HTML o que conta é a presença, não o valor — escrever disabled="false" desabilitaria o botão do mesmo jeito. Por isso o React remove o atributo inteiro. Já o aria-busy={false} fica, escrito como "false": atributo ARIA é string, e ali "false" quer dizer “não está ocupado”, que é diferente de não declarar.

E existe um jeito de montar classe que parece certo e não é:

jsx
function Cartao({ consulta }) {
  return <article className={`cartao ${consulta.atrasada && 'atrasado'}`}>{consulta.pet}</article>;
}

console.log(renderToStaticMarkup(<Cartao consulta={{ pet: 'Nina', atrasada: false }} />));
<article class="cartao false">Nina</article>

Dentro de um template literal, o false que o && devolveu é convertido em texto e vira uma classe chamada false. Como não existe CSS para .false, nada aparece quebrado — o bug fica escondido no HTML até alguém abrir o inspetor. Monte a lista e filtre:

jsx
function CartaoCorrigido({ consulta }) {
  return (
    <article className={['cartao', consulta.atrasada && 'atrasado'].filter(Boolean).join(' ')}>
      {consulta.pet}
    </article>
  );
}

console.log(renderToStaticMarkup(<CartaoCorrigido consulta={{ pet: 'Nina', atrasada: false }} />));
console.log(renderToStaticMarkup(<CartaoCorrigido consulta={{ pet: 'Thor', atrasada: true }} />));
<article class="cartao">Nina</article> <article class="cartao atrasado">Thor</article>

Quando extrair uma condicional para outro componente?

Extraia quando a decisão ganhou nome próprio e o JSX ficou difícil de ler. O sinal não é o tamanho do arquivo: é precisar contar ? e && para entender a tela. Aqui, uma linha decide selo, cancelamento e confirmação no mesmo return:

jsx
function LinhaAntes({ consulta }) {
  return (
    <li>
      {consulta.hora}{consulta.pet}
      {consulta.situacao === 'cancelada' ? (
        <span className="selo selo-cancelada">Cancelada</span>
      ) : consulta.situacao === 'em_atendimento' ? (
        <span className="selo selo-atendimento">Em atendimento</span>
      ) : (
        <span className="selo">Aguardando</span>
      )}
      {consulta.situacao !== 'cancelada' && <button>Cancelar</button>}
      {consulta.situacao === 'aguardando' && <button>Confirmar</button>}
    </li>
  );
}

Cada decisão vira o componente que é dono dela. Selo cuida do texto e da classe; Acoes cuida dos botões, e usa early return para o caso cancelada:

jsx
const CLASSES = { cancelada: 'selo selo-cancelada', em_atendimento: 'selo selo-atendimento' };
const TEXTOS = { cancelada: 'Cancelada', em_atendimento: 'Em atendimento' };

function Selo({ situacao }) {
  return <span className={CLASSES[situacao] ?? 'selo'}>{TEXTOS[situacao] ?? 'Aguardando'}</span>;
}

function Acoes({ situacao }) {
  if (situacao === 'cancelada') return null;
  return (
    <>
      <button>Cancelar</button>
      {situacao === 'aguardando' && <button>Confirmar</button>}
    </>
  );
}

function LinhaDepois({ consulta }) {
  return (
    <li>
      {consulta.hora}{consulta.pet}
      <Selo situacao={consulta.situacao} />
      <Acoes situacao={consulta.situacao} />
    </li>
  );
}

Refatoração de tela precisa de prova, e a prova cabe num laço:

jsx
for (const situacao of ['aguardando', 'em_atendimento', 'cancelada']) {
  const consulta = { hora: '09:00', pet: 'Nina', situacao };
  const antes = renderToStaticMarkup(<LinhaAntes consulta={consulta} />);
  const depois = renderToStaticMarkup(<LinhaDepois consulta={consulta} />);
  console.log(situacao.padEnd(15), antes === depois ? 'HTML idêntico' : 'MUDOU');
}

console.log(
  renderToStaticMarkup(<LinhaDepois consulta={{ hora: '09:00', pet: 'Nina', situacao: 'aguardando' }} />),
);
aguardando HTML idêntico em_atendimento HTML idêntico cancelada HTML idêntico <li>09:00 — Nina<span class="selo">Aguardando</span><button>Cancelar</button><button>Confirmar</button></li>

Byte por byte, a mesma saída — e a última linha mostra o HTML que sobrou. Comparar renderToStaticMarkup antes e depois é o teste mais barato que existe para mexer em condicional de tela, e não pede biblioteca de teste nenhuma.

O que estudar depois de renderização condicional?

O próximo passo é renderizar listas com map e a prop key. É ali que esta decisão ganha escala: você repete cartões, identifica cada item e ainda precisa tratar a lista vazia.

Leve este resumo: early return separa estados; ternário escolhe duas alternativas; && mostra o caso positivo com condição booleana; CSS preserva o elemento e seu estado; um componente novo dá nome à regra que cresceu.

Esse é o passo seguinte da trilha de React. Se quiser ver como listas, estado, eventos e componentes se conectam antes de continuar, o guia completo de React mostra a ordem do caminho.

  • react
  • renderizacao condicional
  • jsx
  • ternario

Perguntas frequentes

Posso escrever if dentro do JSX?
Não. O que fica entre chaves no JSX é uma expressão, e if é uma declaração. Coloque o if antes do return, ou troque por ternário e por &&, que são expressões e cabem entre chaves.
Dá para usar switch para escolher o que renderizar?
Dá, desde que o switch fique antes do return, guardando o resultado numa variável. Para escolher texto ou classe entre poucas opções, um objeto de consulta costuma ficar mais curto e mais fácil de testar do que o switch.
Esconder com CSS é mais rápido que remover do DOM?
Só quando o componente é caro de montar e você alterna muito, tipo uma aba que o usuário fica trocando. Na maioria dos casos o custo de montar de novo é irrelevante e o markup a menos vale mais.
Como esconder um bloco só no celular?
Com media query no CSS, não com condicional em JavaScript. Decidir pela largura da janela dentro do componente faz o servidor renderizar uma coisa e o navegador outra, e isso quebra a hidratação.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Conditional Rendering — react.dev
  2. React — renderToStaticMarkup — react.dev
  3. MDN — Logical AND (&&) — developer.mozilla.org

Continue por aqui