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.
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.
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>,
),
);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.
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 }} />));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:
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:
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:
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.
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:
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:
// 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}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:
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>)));
}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:
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} />));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:
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>));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:
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>
);
}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:
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:
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:
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>;
}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.
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 }} />,
),
);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 é:
function Cartao({ consulta }) {
return <article className={`cartao ${consulta.atrasada && 'atrasado'}`}>{consulta.pet}</article>;
}
console.log(renderToStaticMarkup(<Cartao consulta={{ pet: 'Nina', atrasada: false }} />));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:
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 }} />));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:
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:
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:
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' }} />),
);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.
Perguntas frequentes
Posso escrever if dentro do JSX?
Dá para usar switch para escolher o que renderizar?
Esconder com CSS é mais rápido que remover do DOM?
Como esconder um bloco só no celular?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- React — Conditional Rendering — react.dev
- React — renderToStaticMarkup — react.dev
- MDN — Logical AND (&&) — developer.mozilla.org


