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

JSX no React: escrever HTML dentro do JavaScript

Como o JSX vira chamada de função, por que é className e não class, como interpolar valores com chaves e o que não pode aparecer dentro do JSX.

Rodolfo Mori12 min de leitura

Você abre seu primeiro componente React e encontra algo que parece HTML, mas class virou className, valores ficam entre chaves e duas tags lado a lado derrubam a compilação. A sensação é de que você precisa decorar uma segunda linguagem. Não precisa: existe uma única ideia por trás dessas regras.

JSX é uma extensão de sintaxe do JavaScript que deixa você descrever a tela com tags no meio do código. O navegador não entende JSX. Antes de o arquivo chegar a ele, um compilador — um tradutor de código — troca cada tag por uma chamada de função comum. Nesta lição, você vai enxergar essa tradução, corrigir os erros mais frequentes e terminar sabendo ler JSX sem adivinhar.

Os exemplos aqui são o painel da Escola Aurora, uma escola de idiomas: turmas, vagas e matrícula. Se você ainda não criou o projeto, comece por criar o projeto React com Vite — o Vite já vem com o compilador de JSX ligado.

Por que usar essa sintaxe se ela será transformada? Porque uma tela reúne tags, dados e componentes aninhados, e JSX deixa essa estrutura visível enquanto você programa. JSX é açúcar sintático: uma escrita mais conveniente que o compilador transforma antes da execução. Em palavras simples, você escreve uma árvore parecida com a tela; no momento de execução — o runtime —, o JavaScript trabalha com chamadas e objetos.

O que é JSX e por que o navegador não entende?

JSX é a notação que o compilador transforma em chamadas capazes de criar elementos React; o navegador recebe apenas o resultado dessa transformação.

Imagine um roteiro escrito com marcações para atores e cenário. O JSX é o roteiro legível; as tags são as marcações; os valores entre chaves são as informações variáveis da cena; e o compilador é o tradutor que produz instruções para a equipe executar. O navegador não aprende JSX durante esse processo.

O limite da analogia é importante: o compilador não decide o que a tela deve mostrar e não atualiza o DOM. Ele apenas reescreve a sintaxe. Depois, o React usa os objetos produzidos para renderizar a interface. A progressão desta aula segue essa mesma árvore: primeiro uma tag mínima, depois dados de uma turma e, por fim, formulário, condição e estilo de uma interface real.

Este é um componente da Escola Aurora, num arquivo CardTurma.jsx:

jsx
function CardTurma() {
  return <h2 className="turma">Inglês A2 — terça e quinta</h2>;
}

Parece HTML dentro do JavaScript. Não é. Para ver o que sobra, dá para chamar o compilador na mão, com o Babel:

js
import { transformAsync } from '@babel/core';
import { readFile } from 'node:fs/promises';

const codigo = await readFile('CardTurma.jsx', 'utf8');
const { code } = await transformAsync(codigo, {
  presets: [['@babel/preset-react', { runtime: 'automatic', development: false }]],
  filename: 'CardTurma.jsx',
});
console.log(code);
import { jsx as _jsx } from "react/jsx-runtime"; function CardTurma() { return /*#__PURE__*/_jsx("h2", { className: "turma", children: "Inglês A2 — terça e quinta" }); }

A tag sumiu. No lugar dela ficou _jsx("h2", { ... }): uma função, um nome de tag como texto e um objeto de propriedades. E isso não é um jeito do Babel de fazer as coisas. O esbuild, que é outro compilador, chega ao mesmo formato sozinho:

bash
npx esbuild CardTurma.jsx --jsx=automatic
import { jsx } from "react/jsx-runtime"; function CardTurma() { return /* @__PURE__ */ jsx("h2", { className: "turma", children: "Ingl\xEAs A2 \u2014 ter\xE7a e quinta" }); }

Muda o apelido da função importada — _jsx no Babel, jsx no esbuild — e o esbuild ainda escapa tudo que não é ASCII: \xEA é o ê e \u2014 é o travessão. O formato é o mesmo. E no projeto que você criou com o Vite não é nenhum dos dois: desde a versão 8 quem traduz o JSX é o Oxc Transformer, que já vem embutido, e a saída tem exatamente esta forma. Por isso vale entender a forma uma vez só, em vez de decorar a ferramenta da vez.

E o que essa função devolve? Um objeto JavaScript simples:

js
import { jsx } from 'react/jsx-runtime';
import util from 'node:util';

const elemento = jsx('h2', { className: 'turma', children: 'Inglês A2 — terça e quinta' });

console.log(util.inspect(elemento, { depth: null }));
console.log(typeof elemento);
{ '$$typeof': Symbol(react.transitional.element), type: 'h2', key: null, props: { className: 'turma', children: 'Inglês A2 — terça e quinta' }, _owner: null, _store: {} } object

Guarde essa imagem: <h2> no seu editor é um objeto na memória, com type, key e props. É por isso que você consegue guardar JSX numa variável, passar para uma função e devolver de um if. Não é texto de marcação — é valor.

js
// rodar.mjs — node rodar.mjs arquivo.jsx
import { transformAsync } from '@babel/core';
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';

const entrada = process.argv[2];
const { code } = await transformAsync(await readFile(entrada, 'utf8'), {
  presets: [['@babel/preset-react', { runtime: 'automatic', development: false }]],
  filename: entrada,
});
const saida = entrada.replace(/\.jsx$/, '.gerado.mjs');
await writeFile(saida, code);
await import(pathToFileURL(saida).href);

Como colocar JavaScript dentro do JSX?

Use um par de chaves para abrir uma janela de JavaScript dentro da marcação. Nessa janela cabe uma expressão que produz valor: variável, propriedade de objeto, chamada de método ou conta.

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

const turma = { nome: 'Inglês A2', professora: 'Marina', vagas: 4, mensalidade: 289.9 };

function CardTurma() {
  return (
    <article className="card">
      <h2>{turma.nome}</h2>
      <p>Professora: {turma.professora}</p>
      <p>Restam {turma.vagas} vagas por R$ {turma.mensalidade.toFixed(2)}</p>
      <p>{turma.vagas * 2} lugares na sala</p>
    </article>
  );
}

console.log(renderToStaticMarkup(<CardTurma />));
<article class="card"><h2>Inglês A2</h2><p>Professora: Marina</p><p>Restam 4 vagas por R$ 289.90</p><p>8 lugares na sala</p></article>

O detalhe que explica tudo aparece no código compilado. Peguei só a linha do meio e mandei para o Babel:

jsx
<p>Restam {turma.vagas} vagas por R$ {turma.mensalidade.toFixed(2)}</p>
import { jsxs as _jsxs } from "react/jsx-runtime"; /*#__PURE__*/_jsxs("p", { children: ["Restam ", turma.vagas, " vagas por R$ ", turma.mensalidade.toFixed(2)] });

O texto virou pedaço de um array de filhos, e as chaves viraram as expressões cruas, sem aspas. Repare também que o nome da função mudou de jsx para jsxs — o s é de static children, a versão usada quando há mais de um filho.

Isso é diferente de template literals, que grudam tudo numa string só. Aqui cada pedaço continua sendo um valor separado, e por isso o React consegue trocar só o número de vagas quando ele muda.

Como o que entra nas chaves é um valor, colocar um objeto inteiro estoura:

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

const turma = { nome: 'Inglês A2', professora: 'Marina' };

function CardTurma() {
  return <h2>{turma}</h2>;
}

console.log(renderToStaticMarkup(<CardTurma />));
Error: Objects are not valid as a React child (found: object with keys {nome, professora}). If you meant to render a collection of children, use an array instead.

A mensagem entrega o diagnóstico: ela lista as chaves do objeto que você tentou imprimir. Se aparecerem {nome, professora}, faltou o .nome.

Por que componentes React começam com letra maiúscula?

Porque o compilador usa a inicial para separar uma tag nativa de uma variável que aponta para componente. Essa regra parece capricho de estilo e não é. Veja o que acontece quando o componente começa com minúscula:

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

function cardTurma() {
  return <h2>Inglês A2</h2>;
}

console.log(renderToStaticMarkup(<cardTurma />));
<cardTurma /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements. <cardTurma></cardTurma>

O React avisou e mesmo assim entregou uma tag <cardTurma> vazia — sua função nunca foi chamada. O compilador mostra por quê:

jsx
const a = <cardTurma />;
const c = <CardTurma />;
import { jsx as _jsx } from "react/jsx-runtime"; const a = /*#__PURE__*/_jsx("cardTurma", {}); const c = /*#__PURE__*/_jsx(CardTurma, {});

Minúscula vira "cardTurma", uma string: nome de tag HTML. Maiúscula vira CardTurma, uma variável: a sua função. A decisão é tomada na compilação, antes de o React existir na história.

Por que JSX usa className e htmlFor?

Porque os atributos do JSX viram propriedades de um objeto JavaScript. Como class e for são palavras reservadas da linguagem, o React usa outros nomes. Os atributos compostos seguem camelCase: a primeira palavra começa minúscula e as seguintes começam maiúsculas. Escrevendo do jeito do HTML:

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

function Matricula() {
  return (
    <form class="matricula">
      <label for="aluno">Nome do aluno</label>
      <input id="aluno" maxlength="60" tabindex="1" />
    </form>
  );
}

console.log(renderToStaticMarkup(<Matricula />));
Invalid DOM property `class`. Did you mean `className`? Invalid DOM property `for`. Did you mean `htmlFor`? Invalid DOM property `maxlength`. Did you mean `maxLength`? Invalid DOM property `tabindex`. Did you mean `tabIndex`? <form class="matricula"><label for="aluno">Nome do aluno</label><input id="aluno" maxlength="60" tabindex="1"/></form>

Repare que o React 19 avisa quatro vezes e renderiza mesmo assim. É o pior dos mundos: a tela funciona, o console enche e o problema atravessa a revisão de código. Trate esse aviso como erro.

A versão certa:

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

function Matricula() {
  return (
    <form className="matricula">
      <label htmlFor="aluno">Nome do aluno</label>
      <input id="aluno" maxLength={60} tabIndex={1} />
    </form>
  );
}

console.log(renderToStaticMarkup(<Matricula />));
<form class="matricula"><label for="aluno">Nome do aluno</label><input id="aluno" maxLength="60" tabindex="1"/></form>

Os que você mais vai encontrar:

no HTML no JSX por quê
class className class é palavra reservada do JavaScript
for htmlFor for é palavra reservada do JavaScript
maxlength maxLength atributo composto vira camelCase
tabindex tabIndex atributo composto vira camelCase
onclick onClick evento composto vira camelCase
readonly readOnly atributo composto vira camelCase
data-turma data-turma data- e aria- não mudam

Por que toda tag precisa ser fechada no JSX?

Porque o compilador precisa saber exatamente onde cada elemento termina. Em HTML você escreve <img> e o navegador entende. Em JSX, a tag precisa terminar com o fechamento correspondente.

jsx
function Foto() {
  return (
    <figure>
      <img src="/turmas/ingles-a2.jpg" alt="Turma de Inglês A2">
      <figcaption>Turma de 2026</figcaption>
    </figure>
  );
}
SyntaxError: /private/tmp/escola/autofechada.jsx: Unexpected token `}`. Did you mean `&rbrace;` or `{'}'}`? (8:0)

6 | </figure> 7 | ); > 8 | } | ^ 9 |

O erro está na linha 4 e o compilador aponta a linha 8. Faz sentido do ponto de vista dele: sem a barra, tudo que vem depois de <img ...> é considerado conteúdo da tag, e ele só percebe que algo está errado quando o arquivo acaba sem o fechamento. Quando o erro de JSX apontar para a última linha, procure a tag aberta mais acima. A correção é <img ... />.

Como retornar dois elementos sem criar uma div?

Use um Fragment, um agrupador que não cria elemento extra no HTML. Ele é necessário porque um return devolve um valor, enquanto duas tags soltas lado a lado representam dois valores e nem chegam a compilar:

jsx
function Cabecalho() {
  return (
    <h2>Inglês A2</h2>
    <p>Terça e quinta, 19h</p>
  );
}
SyntaxError: /private/tmp/escola/dois-elementos.jsx: Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>? (4:4)

2 | return ( 3 | <h2>Inglês A2</h2> > 4 | <p>Terça e quinta, 19h</p> | ^ 5 | ); 6 | }

A solução óbvia é embrulhar numa <div>. O problema é que essa div vai parar no HTML e atrapalha o layout, principalmente dentro de Flexbox ou Grid. O Fragment resolve: <> e </>, um embrulho que não vira elemento nenhum.

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

function Cabecalho() {
  return (
    <>
      <h2>Inglês A2</h2>
      <p>Terça e quinta, 19h</p>
    </>
  );
}

console.log(renderToStaticMarkup(<Cabecalho />));
<h2>Inglês A2</h2><p>Terça e quinta, 19h</p>

Nenhuma tag extra na saída. Quando você precisa de key — o caso de listas renderizadas com map — a forma curta não serve, porque ela não aceita atributo. Aí se escreve <Fragment key={id}>, importando Fragment do react.

Quais valores aparecem ou somem no JSX?

false, null, undefined e string vazia não deixam texto visível; números, inclusive zero, aparecem. Para observar a regra inteira, coloquei sete valores diferentes na mesma lista:

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

function Linha({ rotulo, valor }) {
  return <li>{rotulo}: {valor}</li>;
}

function Diagnostico() {
  return (
    <ul>
      <Linha rotulo="false" valor={false} />
      <Linha rotulo="null" valor={null} />
      <Linha rotulo="undefined" valor={undefined} />
      <Linha rotulo="string vazia" valor={''} />
      <Linha rotulo="zero" valor={0} />
      <Linha rotulo="NaN" valor={NaN} />
      <Linha rotulo="array" valor={['a', 'b']} />
    </ul>
  );
}

console.log(renderToStaticMarkup(<Diagnostico />));
<ul><li>false: </li><li>null: </li><li>undefined: </li><li>string vazia: </li><li>zero: 0</li><li>NaN: NaN</li><li>array: ab</li></ul>
valor aparece na tela consequência prática
false, null, undefined nada é o que permite esconder algo com &&
'' nada string vazia não deixa buraco
0 0 número é número: o zero é desenhado
NaN NaN conta que deu errado vaza para o usuário
['a', 'b'] ab array é achatado e cada item é renderizado

Essa lista é a origem de um bug clássico. O painel mostra as vagas restantes só quando existem vagas:

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

function Vagas({ vagas }) {
  return (
    <div className="vagas">
      {vagas && <p>Restam {vagas} vagas</p>}
    </div>
  );
}

console.log(renderToStaticMarkup(<Vagas vagas={4} />));
console.log(renderToStaticMarkup(<Vagas vagas={0} />));
<div class="vagas"><p>Restam 4 vagas</p></div> <div class="vagas">0</div>

Com a turma cheia, um 0 solto apareceu na tela. O && devolve o valor da esquerda quando ele é falsy, e 0 é falsy — mas, ao contrário de false, o React desenha o zero. A correção é transformar a condição em booleano. Trocando só a linha do &&, no mesmo componente acima:

jsx
{vagas > 0 && <p>Restam {vagas} vagas</p>}
<div class="vagas"><p>Restam 4 vagas</p></div> <div class="vagas"></div>

Como usar style no JSX?

Passe um objeto JavaScript para style, não uma string de CSS. Por isso copiar o atributo diretamente do HTML não funciona:

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

function Aviso() {
  return <p style="color: crimson; font-weight: bold">Turma quase cheia</p>;
}

console.log(renderToStaticMarkup(<Aviso />));
Error: The `style` prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.

Aqui o React não avisa: ele derruba a renderização. style espera um objeto, com as propriedades em camelCase:

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

function Aviso({ vagas }) {
  const estilo = {
    color: vagas <= 2 ? 'crimson' : 'seagreen',
    fontWeight: 'bold',
    paddingInline: 8,
    borderRadius: 4,
  };

  return <p style={estilo}>Restam {vagas} vagas</p>;
}

console.log(renderToStaticMarkup(<Aviso vagas={2} />));
console.log(renderToStaticMarkup(<Aviso vagas={9} />));
<p style="color:crimson;font-weight:bold;padding-inline:8px;border-radius:4px">Restam 2 vagas</p> <p style="color:seagreen;font-weight:bold;padding-inline:8px;border-radius:4px">Restam 9 vagas</p>

Duas coisas para guardar dessa saída. fontWeight voltou a ser font-weight e paddingInline virou padding-inline: a conversão é automática. E o número 8 ganhou px sozinho — o React completa a unidade nas propriedades que aceitam comprimento. Em lineHeight ou flexGrow, que não são comprimento, o número fica cru.

Quando você escreve style={{ ... }} direto na tag, o par externo de chaves é a janela do JSX e o interno é o objeto literal. Não são chaves duplas: são duas coisas diferentes coladas.

Como escrever comentários dentro do JSX?

Abra uma expressão com chaves e coloque dentro dela um comentário de bloco. Escrever // diretamente no meio da marcação não comenta nada — vira texto na tela:

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

function Rodape() {
  return (
    <footer>
      // matrícula aberta até sexta
      <p>Escola Aurora</p>
    </footer>
  );
}

console.log(renderToStaticMarkup(<Rodape />));
<footer>// matrícula aberta até sexta<p>Escola Aurora</p></footer>

Faz sentido: dentro de uma tag, o compilador está lendo conteúdo, não código. Para comentar, abra a janela do JavaScript primeiro e use o comentário de bloco:

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

function Rodape() {
  return (
    <footer>
      {/* matrícula aberta até sexta */}
      <p>Escola Aurora</p>
    </footer>
  );
}

console.log(renderToStaticMarkup(<Rodape />));
<footer><p>Escola Aurora</p></footer>

O comentário de HTML, <!-- -->, não existe em JSX: ele quebra a compilação com Unexpected token.

Por que if solto entre chaves não compila?

Porque as chaves aceitam uma expressão — algo que produz um valor. if, for e switch são instruções: mandam fazer, não produzem valor. Por isso isto nem compila:

jsx
function StatusTurma({ vagas }) {
  return (
    <p>
      {if (vagas > 0) { 'Matrícula aberta' } else { 'Turma cheia' }}
    </p>
  );
}
SyntaxError: /private/tmp/escola/if-solto.jsx: Unexpected token (4:7)

2 | return ( 3 | <p> > 4 | {if (vagas > 0) { ‘Matrícula aberta’ } else { ‘Turma cheia’ }} | ^ 5 | </p> 6 | );

O ternário é a versão-expressão do if, e cabe dentro das chaves:

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

function StatusTurma({ vagas }) {
  return <p>{vagas > 0 ? 'Matrícula aberta' : 'Turma cheia'}</p>;
}

console.log(renderToStaticMarkup(<StatusTurma vagas={4} />));
console.log(renderToStaticMarkup(<StatusTurma vagas={0} />));
<p>Matrícula aberta</p> <p>Turma cheia</p>

Se a decisão for maior que um ternário, use if de verdade — só que fora do return, guardando o JSX numa variável. Como você viu na primeira seção, JSX é valor: dá para atribuir, comparar e devolver. As formas completas estão em renderização condicional no React.

O que estudar depois de JSX?

O próximo passo é retirar os dados fixos de dentro do componente e recebê-los de fora. Agora você sabe ler o que o compilador faz com a sua marcação e reconhecer os quatro erros que mais aparecem no começo: class em vez de className, tag sem fechar, dois elementos sem Fragment e o zero do &&. Mas os componentes daqui ainda têm os dados chumbados dentro deles. Continue em passar dados com props, para o mesmo CardTurma servir a todas as turmas da Escola Aurora. Se quiser ver a ordem inteira antes de continuar, o guia de React mostra onde cada assunto entra.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • react
  • jsx
  • componentes
  • babel
  • vite

Perguntas frequentes

Preciso instalar o Babel para usar JSX?
Não. O Babel é um dos compiladores que entendem JSX, e é o que eu usei aqui para mostrar a saída passo a passo. Num projeto criado com o Vite 8, quem transforma o JSX é o Oxc Transformer, que já vem embutido — você não instala nem configura nada.
Dá para usar React sem escrever JSX?
Dá. JSX é açúcar sintático: você pode chamar jsx() ou React.createElement() na mão e o resultado é idêntico. Ninguém faz isso em projeto de tela porque a marcação fica ilegível em três níveis de aninhamento.
O arquivo do componente precisa ter extensão .jsx?
No Vite, sim. Por padrão ele só liga a sintaxe JSX em arquivos .jsx e .tsx; o mesmo código dentro de um .js derruba o build com Unexpected JSX expression. Renomeie o arquivo em vez de mexer na configuração.
Como coloco duas classes no className?
A prop recebe uma string só, então monte a string antes de entregar — com um template literal ou com uma variável calculada acima do return. Quando as condições passam de duas, vale usar uma biblioteca como a clsx.

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, React 19.2.8, @babel/core 8.0.1, esbuild 0.28.2, Vite 8.2.2, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. React — Writing Markup with JSX — react.dev
  2. React — JavaScript in JSX with Curly Braces — react.dev
  3. React — Writing markup with JSX — react.dev
  4. Vite — Features: JSX — vite.dev

Continue por aqui