Dia 10 — JSX e o primeiro componente
O que e JSX e por que existe
O que é JSX e por que existe
A sintaxe JSX é o que o JavaScript entende e o transformador converte em chamada
de função. Não é HTML, não é XML — apesar do nome, JavaScript XML é o nome do
formato, não uma linguagem. O JSX existe porque descrever tela em árvore de tags
é mais curto que descrever a mesma árvore em chamadas aninhadas.
const { View, Text } = require('react-native');
O que o <Text>ola</Text> realmente é, depois do Babel:
h(Text, null, 'ola');
A tag vira função, o conteúdo entre a tag vira argumento. É por isso que Text
precisa estar importada: View e Text são funções do JavaScript, não tags de um
HTML que não existe aqui.
Componente: a função que devolve JSX
Um componente é uma função que devolve JSX. Não há classe, não há
extends, não há construtor:
function Cartao() { return ( <View> <Text>ola</Text> </View> ); }
Os parênteses não são obrigatórios com um elemento só, mas são com mais de um:
sem eles, o JavaScript leria return seguido de outro comando, e o
SyntaxError sai em Unexpected token.
O componente é chamado pelo runtime, não pelo aluno. Quem chama é o React Native,
quando a tela precisa ser desenhada — e quando o estado muda, ele chama de novo.
O exemplo chama Cartao à mão e lê o que ela devolve, porque não existe
renderizador aqui — mas o objeto devolvido é exatamente o que o React Native
receberia. A primeira linha é nome do componente: Cartao, que vem de
Cartao.name: o nome da função sobrevive no objeto devolvido, e é ele que
aparece quando algo dá erro dentro dela.
A segunda linha, o componente devolveu: View, é a conversão do parágrafo. O
<View> da função virou um objeto cujo .type é a string 'View'. E a terceira
imprime props do View inteiro, com o children sendo outro elemento e não
texto — porque o que foi posto dentro da tag foi <Text>ola</Text>.
Expressão dentro do JSX
Toda expressão em JSX vai entre um par de chaves: {}: as { } marcam expressão de JavaScript, e podem ir em
três lugares:
<Text>{tarefa.texto}</Text> <Text>{total > 0 ? 'tem' : 'vazio'}</Text> <Text style={{ fontSize: 18 }}>18</Text>
A primeira linha é o valor da variável, a segunda é uma expressão inteira, e a
terceira é um objeto — que precisa de dois pares de chaves, porque o primeiro par
é a marcação de expressão e o segundo é o objeto.
O exemplo mostra a segunda dessas três, texto do filho: ola. O <Text>ola</Text>
tem o texto escrito direto entre as tags, sem chaves — e mesmo assim o
children dele é 'ola'. É a diferença entre o texto solto e o { }: os dois
viram children, e o solto vira string sem passar por expressão nenhuma.
Quando o conteúdo vem de fora, é o { } que faz o trabalho: filho.props.children
seria o valor da variável, não o nome dela. A regra é que tudo que não for
literal de texto precisa estar entre chaves.
Fragmento: dois elementos sem container
Quando o componente precisa devolver dois elementos irmãos, ele precisa de um
container. O fragmento (<>) é esse container que não gera nada na tela:
function Cabecalho() { return ( <> <Text>titulo</Text> <Text>subtitulo</Text> </> ); }
A forma longa é <React.Fragment>. O atalho <> faz o mesmo e é o que se usa
por padrão. Sem o fragmento, a solução seria um View a mais — e ele muda o
desenho, porque View é flex container e empilha os filhos.
children e a chave de lista
O que vai dentro da tag chega na prop children:
<Cartao titulo="Estatísticas"> <Text>12 tarefas</Text> </Cartao>
E quando o elemento está dentro de um map, ele precisa de chave (key).
A chave é o id do item, nunca o índice — o índice anda quando a lista muda de
ordem, e é esse descasamento que faz dois itens trocarem de texto na tela.
O exemplo não usa map — ele monta um elemento só — e é por isso que não tem
key em lugar nenhum. A chave só é exigida quando existem vários irmãos
nascidos da mesma volta do laço. Um elemento único não precisa dela, e
props do View no exemplo sai só com children, sem key.
O uso da chave e do children juntos vem da próxima aula, no componente que
recebe conteúdo por tag e lista os itens por map com key={tarefa.id}.
O que JSX não é
Não é string: '<Text>ola</Text>' é texto de sete caracteres, e não vira
elemento. Não é interpretado em tempo de execução — quem converte é o Metro, no
desenvolvimento, e o Babel no pacote final. Por isso o erro de sintaxe
aparece em tempo de compilação, antes de o app rodar.
E o detalhe de ambiente que este material usa: um arquivo .js com JSX não roda
no Node puro, porque node --check lê <View> como token inesperado. Não é erro
do exemplo — é ausência do transformador. No app quem converte é o Metro.
E é exatamente isso que o exemplo da página mostra: o arquivo tem JSX e roda
mesmo assim, porque existe o passo de transformação antes de o Node ver o
código. Quem rodar o mesmo arquivo pelo caminho comum vai ver
SyntaxError: Unexpected token '<', e o diagnóstico correto é "falta o
Babel", não "falta parêntese".
O props impresso pelo exemplo é a prova do outro lado: ele mostra
'$$typeof': Symbol(react.element) dentro do objeto. Esse símbolo é o que
distingue um elemento de React de um objeto comum, e ele só existe porque o
JSX foi convertido antes.
Exemplo
// Exemplo da aula "O que e JSX e por que existe". // // JSX nao e HTML: e uma sintaxe que o JavaScript entende e o // transformador converte em chamadas de função. Um componente e uma // função que devolve JSX; o React Native chama essa função e desenha o // que ela devolveu. const { View, Text } = require('react-native'); // Componente: uma funcao que devolve JSX. Os parenteses nao sao // obrigatorios, mas com varios elementos eles sao — sem eles, o // JavaScript leria `return` seguido de outro comando. function Cartao() { return ( <View> <Text>ola</Text> </View> ); } console.log('nome do componente:', Cartao.name); // O que o componente devolve? Aqui nao tem renderizador, entao o exemplo // chama a funcao e le o objeto devolvido, que e o mesmo objeto que o // React Native receberia. const arvore = Cartao(); console.log('o componente devolveu:', arvore.type); console.log('props do View:', arvore.props); // `props.children` e o que foi posto DENTRO do elemento. const filho = arvore.props.children; console.log('o View tem um filho:', filho.type); console.log('texto do filho:', filho.props.children);
Saída real
nome do componente: Cartao
o componente devolveu: View
props do View: {
children: {
'$$typeof': Symbol(react.element),
type: 'Text',
props: { children: 'ola' },
children: null
}
}
o View tem um filho: Text
texto do filho: ola
View, Text e StyleSheet
View, Text e StyleSheet
View é o retângulo que não diz nada. Text é o que escreve texto. Entre os dois
não existe div, p nem span: no React Native, **texto fora de Text não
aparece**, e é o erro de tela em branco que o aluno mais encontra.
const { View, Text } = require('react-native');
A regra que resolve quase tudo: todo texto mora dentro de um Text, e todo
Text mora dentro de um View (ou dentro de outro Text).
StyleSheet: o objeto de estilo
Criar estilo é escrever um objeto JavaScript com propriedades de caixa: flex,
padding, margin, fontSize, color, backgroundColor, borderRadius. Os
nomes são em camelCase, com a mesma semântica de CSS — e o que não muda é o
comportamento padrão, que não é o do navegador: todo View é display: flex
por padrão, e é por isso que filhos empilham em coluna sem nenhuma propriedade.
StyleSheet.create recebe o objeto e devolve o mesmo. A forma recomendada de usar
é uma constante styles fora do componente:
const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: '#fff', }, }); function Tela() { return <View style={styles.container}><Text>Ola</Text></View>; }
style recebe o nome do estilo: style={styles.container}. E a forma
inline — objeto escrito direto no JSX — serve para o valor que muda:
<Text style={{ fontSize: 18, color: '#222' }}>{tarefa.texto}</Text>
O exemplo tem os cinco estilos declarados e o console confirma que existem
todos: `styles disponiveis: [ 'container', 'linha', 'titulo', 'apagado',
'ativo' ]. O Object.keys` é a conferência — e ela mostra que o
StyleSheet.create devolveu um objeto comum, com uma chave por nome declarado.
A forma inline do exemplo é style inline: { flexDirection: 'row', gap: 8 },
com aspas duplas e dois pares de chaves. O valor tem a mesma forma do que está
no StyleSheet — a diferença é onde ele foi escrito: dentro do JSX, na hora da
renderização, e não uma vez para o arquivo inteiro.
Object literal dentro de JSX usa aspas duplas e dois pontos: {{ ... }}. Com
aspas simples e um só par de chaves, a chave vira string e o estilo não é
aplicado — o componente renderiza sem estilo e sem erro nenhum.
As propriedades que resolvem 90% da tela
| Propriedade | O que faz |
|---|---|
flex: 1 | ocupa o espaço que sobrou na direção principal |
flexDirection | 'row' na horizontal, 'column' (padrão) na vertical |
justifyContent | distribui ao longo do eixo principal |
alignItems | alinha no eixo cruzado |
padding / margin | espaço interno / externo |
gap | espaço entre filhos, sem margem em cada um |
backgroundColor | cor de fundo |
borderRadius | raio dos cantos |
flexDirection: 'row' é o que põe lado a lado — e é a propriedade mais usada em
lista: uma linha com avatar, título e botão.
A árvore do exemplo é o que mostra a tabela em uso. Ela chama o componente
Tela e imprime quatro linhas: tipo da raiz: View,
style da raiz: { flex: 1, padding: 16, backgroundColor: '#fff' },
filho: View | style do filho: { flexDirection: 'row', alignItems: 'center' } e
texto: Estudar.
A última linha é a mais informativa: o caminho até o texto tem três .props
no meio. A raiz é um View; o primeiro .props.children é o View de dentro;
o segundo .props.children é o Text; e o quarto é o texto. São três níveis
de aninhamento para chegar na palavra, e é assim que o JSX vira um objeto
aninhado. Quem debuga tela em branco começa por essa linha: ela mostra o que a
tela montou.
Estilo pode ser array
Quando o estilo vem de uma parte fixa e uma variável, style aceita array,
que o runtime combina na ordem — o último ganha:
<View style={[styles.cartao, ativo && styles.ativo]}>
O && vem porque false não tem estilo: quando a condição é falsa, o item do
array é false e é ignorado. StyleSheet.flatten é o método que transforma esse
array num objeto só, e é ele que devolve { padding: 16 } quando dois estilos
disputam a mesma propriedade.
As quatro linhas do exemplo mostram o array antes e depois do combinado.
style como array: imprime os dois objetos separados por vírgula, com o
container inteiro e o ativo abaixo. `flatten junta num objeto so: { flex: 1,
padding: 16, backgroundColor: '#e8f2ff' }` mostra o resultado: as quatro
propriedades do container e o backgroundColor substituído pelo do ativo.
O && é testado nas duas linhas seguintes, e elas são o par que fixa o
comportamento: com condicional verdadeira: { backgroundColor: '#e8f2ff' } e
com condicional falsa: false. A primeira posição do array recebeu o objeto; a
segunda recebeu false. O runtime ignora o false e combina o resto — é por
isso que false pode ficar no array sem precise de filtro.
O erro de tela em branco
Texto sem Text não renderiza. Estilo com { } só vira string. color sem
contraste dá texto branco em fundo branco — existe, mas some. E
StyleSheet.hairlineWidth não existe: a largura de borda fina é o número 1.
Para descobrir qual dos quatro é, o caminho é o mesmo: colocar console.log
dentro do componente e ler .props do elemento devolvido.
O exemplo fecha com a regra do texto dentro do Text. O componente Cartao
devolve um View com dois Text dentro, e a linha
dois textos: [ 'Treinar', 'hoje as 7' ] mostra os dois textos lidos pelo
map sobre props.children. O children com dois filhos é um array, e é
por isso que o .map funciona — com um filho só, children seria o elemento
único, e não uma lista.
Exemplo
// View, Text e StyleSheet: o componente que monta a tela e o objeto de // estilo que descreve o desenho. const { View, Text, StyleSheet } = require('react-native'); /* StyleSheet.create recebe o objeto e devolve o mesmo */ const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: '#fff', }, linha: { flexDirection: 'row', alignItems: 'center', }, titulo: { fontSize: 18, color: '#222222', }, apagado: { fontSize: 14, color: '#888888', }, ativo: { backgroundColor: '#e8f2ff', }, }); function Tela() { return ( <View style={styles.container}> <View style={styles.linha}> <Text style={styles.titulo}>Estudar</Text> </View> </View> ); } const arvore = Tela(); console.log('tipo da raiz:', arvore.type); console.log('style da raiz:', arvore.props.style); console.log('filho:', arvore.props.children.type, '| style do filho:', arvore.props.children.props.style); console.log('texto:', arvore.props.children.props.children.props.children); /* style inline: objeto com dois pontos e aspas duplas */ const linha = View({ style: { flexDirection: 'row', gap: 8 } }); console.log('style inline:', linha.props.style); /* estilo pode ser array: o ultimo ganha */ const misto = View({ style: [styles.container, styles.ativo] }); console.log('style como array:', misto.props.style); console.log('flatten junta num objeto so:', StyleSheet.flatten([styles.container, styles.ativo])); /* o && entra porque false nao tem estilo: e ignorado no array */ const destaque = true; const comCondicional = View({ style: [styles.container, destaque && styles.ativo] }); console.log('com condicional verdadeira:', comCondicional.props.style[1]); const semCondicional = View({ style: [styles.container, false && styles.ativo] }); console.log('com condicional falsa:', semCondicional.props.style[1]); /* texto sempre dentro de Text */ function Cartao({ titulo, subtitulo }) { return ( <View style={styles.container}> <Text style={styles.titulo}>{titulo}</Text> <Text style={styles.apagado}>{subtitulo}</Text> </View> ); } const cartao = Cartao({ titulo: 'Treinar', subtitulo: 'hoje as 7' }); console.log('dois textos:', cartao.props.children.map((f) => f.props.children)); console.log('styles disponiveis:', Object.keys(styles));
Saída real
tipo da raiz: View
style da raiz: { flex: 1, padding: 16, backgroundColor: '#fff' }
filho: View | style do filho: { flexDirection: 'row', alignItems: 'center' }
texto: Estudar
style inline: { flexDirection: 'row', gap: 8 }
style como array: [
{ flex: 1, padding: 16, backgroundColor: '#fff' },
{ backgroundColor: '#e8f2ff' }
]
flatten junta num objeto so: { flex: 1, padding: 16, backgroundColor: '#e8f2ff' }
com condicional verdadeira: { backgroundColor: '#e8f2ff' }
com condicional falsa: false
dois textos: [ 'Treinar', 'hoje as 7' ]
styles disponiveis: [ 'container', 'linha', 'titulo', 'apagado', 'ativo' ]