Dia 11 — Props e composição de componentes
Props: passar dado para o componente
Props: passar dado para o componente
A propriedade do componente é a prop. Props são os dados que um componente
recebe de quem o usa. É o caminho único
de informação de um nível da árvore para o outro: o pai monta o componente com
propriedades, o filho lê.
<Rotulo texto="Estudar" />
Passar props é escrever o nome entre a tag e o valor entre aspas ou entre
chaves. O componente recebe as props como um único objeto, no primeiro
parâmetro:
function Rotulo({ texto, cor }) { return <Text style={{ color: cor }}>{texto}</Text>; }
Os { } ali dentro do parâmetro são o destructuring de props: a
desestruturação das props, em que o nome de
cada variável é o nome da prop. É a forma quase obrigatória em React Native,
porque componente costuma receber cinco ou seis props e ler props.texto
inteiro o tempo todo cansa.
O exemplo mostra essa leitura em uma linha: props nomeadas: Estudar. O
componente devolveu um Text, e o .props.children dele é 'Estudar' — o
valor da prop texto, e não o nome dela. A linha chama o componente passando um
objeto comum, o que o runtime faz quando um pai escreve `<Rotulo texto="Estudar"
/>`.
O detalhe é que a função não tem nome de parâmetro: o primeiro parâmetro é o
objeto das props, e o { texto, cor } é a desestruturação dele. Chamar
Rotulo({ texto: 'Estudar' }) e Rotulo({ texto: 'Estudar', cor: '#222' })
funciona igual — o que não for passado simplesmente não existe no objeto.
children: a prop que vem do conteúdo
O que é posto dentro da tag chega na prop children, sem precisar nomear:
<Rotulo>Estudar</Rotulo> // children = 'Estudar'
Equivale a passar children à mão. As duas formas funcionam, e a diferença é
quando o conteúdo é longo: com children, o texto fica visível dentro da tag.
Como children é uma prop como outra qualquer, dá para dar valor padrão:
function Cartao({ titulo, children = 'sem conteúdo' }) { return <View><Text>{titulo}</Text>{children}</View>; }
E dá para tratar os dois casos: quando o pai não manda nada, o componente
mostra o conteúdo padrão.
O exemplo escreve o par nas duas linhas: children por tag: 2 dias e
children padrao: sem conteudo. A primeira é o Cartao recebendo a prop
children com o texto '2 dias'; a segunda é o mesmo componente sem a prop, e o
padrão entrou no lugar. A leitura é props.children[1] — o índice 1 porque o
View tem dois filhos: o Text do título e o conteúdo.
O índice 1 é o detalhe que costuma travar: o children de um elemento com
vários filhos é um array, e é por isso que ele se lê com colchete. Com um
filho só, children seria o elemento, e children[1] seria undefined.
Valor padrão e nome opcional
Prop que nem sempre vem precisa de valor padrão, senão o que falta é undefined:
function Botao({ texto, tipo = 'primario', aoClicar }) { return <Pressable onPress={aoClicar}><Text>{texto}</Text></Pressable>; }
O padrão só entra se a prop não vier. Se vier undefined explicitamente, o
padrão entra também; se vier null, não — o null é um valor real.
Prop cujo nome muda de nome entre o pai e o filho é só a renomeação da
desestruturação: { texto: titulo }.
As duas linhas do exemplo tratam dos dois casos do parágrafo e da renomeação.
padrao: primario | quando a prop vem: secundario mostra o padrão entrando e
sendo substituído. null e um valor real, nao entra o padrao: null é o null
que o parágrafo avisa: ele passou direto, e o 'primario' não entrou.
E renomeado: Treinar | sem titulo mostra o nome trocado com o padrão junto: o
pai mandou texto, o filho leu titulo, e sem prop nenhuma o padrão
'sem titulo' apareceu. O : e o = convivem na mesma chave, que é a mesma
combinação da aula de desestruturação.
Espalhar as props
O spread de props passa tudo o que chegou, sem escrever cada nome:
<View {...props} />
É o que permite um componente que serve de caixa transparente: ele recebe o que
o pai mandou e repassa inteiro, acrescentando o que é dele:
function Linha({ children, ...resto }) { return <View style={styles.linha} {...resto}>{children}</View>; }
O ...resto é o rest da aula de desestruturação: tudo que não foi pego pelo
nome vai para resto. O resto tem que vir depois das props nomeadas, senão
sobrescreve o que o componente definiu.
A linha do exemplo é props repassadas: [ 'onPress', 'testID', 'children' ]. O
Caixa pegou children pelo nome e o resto foi repassado com ...resto; o
Object.keys do props devolvido mostra as três chaves que chegaram ao
View de dentro. O children aparece na lista porque ele também estava no
objeto de entrada.
Repare no que o resto não traz: o style que o Caixa definiu no View
não aparece nessa lista, porque o Object.keys foi chamado no elemento **de
fora**, que só recebeu o repassado. É a linha do parágrafo sobre a ordem: se o
{...resto} viesse antes do style, o resto escreveria por cima.
Props são só de leitura
Prop é entrada do componente. O filho lê, mas quem muda é o pai — quem avisa é o
componente filho, chamando a função que recebeu por prop:
<Item tarefa={tarefa} aoConcluir={() => marcarFeita(tarefa.id)} />
É a regra que faz o estado funcionar: o Item não guarda se a tarefa está feita.
Ele recebe tarefa.feita, desenha conforme o valor, e avisa a intenção pelo
aoConcluir. Quem guarda o dado novo é o componente que tem o useState.
A consequência prática é que uma prop nunca deve ser reescrita: `props.texto =
'x'` não muda nada na tela, porque o valor vem do pai e o React Native redesenha
quando o pai passa prop nova.
O exemplo fecha os três pontos da regra. `item montado: [ 'estudar',
'pendente' ] mostra o Item` desenhando exatamente o que veio na prop: o texto
da tarefa e o status derivado de tarefa.feita. avisos recebidos pelo pai: [ 1 ]
mostra o caminho do outro lado — o pai entregou a função aoConcluir, o filho a
chamou, e o 1 que apareceu é o id que estava dentro dela. E
o filho so desenha o que veio de prop: [ 'estudar', 'pendente' ] repete a
primeira linha depois do aviso, para mostrar que o filho não guardou nada.
A última linha é a de leitura: `prop antes: original | o que o filho viu:
original. O pai tinha um objeto com texto: 'original'`, o filho recebeu o
mesmo valor, e nenhuma escrita aconteceu no meio. Para mudar o texto, quem
escreve é o pai — passando prop nova, que é o que dispara o redesenho.
Exemplo
// Props: como o pai passa dado para o filho, e como o filho le. const { View, Text, StyleSheet } = require('react-native'); const styles = StyleSheet.create({ linha: { flexDirection: 'row', alignItems: 'center', padding: 12 }, titulo: { fontSize: 16, color: '#222222' }, }); /* props nomeadas: o primeiro parametro e o objeto das props */ function Rotulo({ texto, cor }) { return <Text style={{ color: cor }}>{texto}</Text>; } console.log('props nomeadas:', Rotulo({ texto: 'Estudar', cor: '#222' }).props.children); /* valor padrao do parametro */ function tipoDoBotao({ tipo = 'primario' }) { return tipo; } console.log('padrao:', tipoDoBotao({}), '| quando a prop vem:', tipoDoBotao({ tipo: 'secundario' })); console.log('null e um valor real, nao entra o padrao:', tipoDoBotao({ tipo: null })); /* children: o que vai dentro da tag */ function Cartao({ titulo, children = 'sem conteudo' }) { return ( <View style={styles.linha}> <Text style={styles.titulo}>{titulo}</Text> {children} </View> ); } console.log('children por tag:', Cartao({ titulo: 'Estudar', children: '2 dias' }).props.children[1]); console.log('children padrao:', Cartao({ titulo: 'Estudar' }).props.children[1]); /* renomear a prop na desestruturacao */ function Card({ texto: titulo = 'sem titulo' }) { return <Text>{titulo}</Text>; } console.log('renomeado:', Card({ texto: 'Treinar' }).props.children, '|', Card({}).props.children); /* spread de props e rest: repassar tudo sem escrever cada nome */ function Caixa({ children, ...resto }) { return <View {...resto}>{children}</View>; } const repassado = Caixa({ children: 'conteudo', onPress: () => {}, testID: 'caixa' }); console.log('props repassadas:', Object.keys(repassado.props)); /* o pai monta o filho com os dados */ const tarefa = { id: 1, texto: 'estudar', feita: false }; function Item({ tarefa, aoConcluir }) { return ( <View style={styles.linha}> <Text style={styles.titulo}>{tarefa.texto}</Text> <Text>{tarefa.feita ? 'feita' : 'pendente'}</Text> </View> ); } const item = Item({ tarefa, aoConcluir: () => {} }); console.log('item montado:', item.props.children.map((f) => f.props.children)); /* a funcao de aviso: o pai e quem muda o dado, o filho so avisa */ const avisos = []; const marcarFeita = () => avisos.push(tarefa.id); const comAviso = Item({ tarefa, aoConcluir: marcarFeita }); // o filho recebe a funcao como prop e a chama: e isso que avisa o pai marcarFeita(); console.log('avisos recebidos pelo pai:', avisos); console.log('o filho so desenha o que veio de prop:', comAviso.props.children.map((f) => f.props.children)); /* prop reescrita por dentro nao muda nada: quem manda e o pai */ function Recurso({ texto }) { return <Text>{texto}</Text>; } const props = { texto: 'original' }; console.log('prop antes:', props.texto, '| o que o filho viu:', Recurso(props).props.children);
Saída real
props nomeadas: Estudar padrao: primario | quando a prop vem: secundario null e um valor real, nao entra o padrao: null children por tag: 2 dias children padrao: sem conteudo renomeado: Treinar | sem titulo props repassadas: [ 'onPress', 'testID', 'children' ] item montado: [ 'estudar', 'pendente' ] avisos recebidos pelo pai: [ 1 ] o filho so desenha o que veio de prop: [ 'estudar', 'pendente' ] prop antes: original | o que o filho viu: original
Composição e componente reutilizável
Composição e componente reutilizável
Composição é montar tela juntando componente pequeno, em vez de escrever um
componente grande. A árvore do app é uma árvore de peças: o componente da tela usa
um componente de linha, que usa um componente de botão.
Reutilizar é o objetivo: componente reutilizável é o que serve em mais de um lugar. A pergunta que
decide se vale extrair é "isto depende do nome da tela?" — se não depende, é o
componente que se repete.
function Botao({ texto, aoClicar, tipo = 'primario' }) { }
O mesmo componente entra em duas telas sem ser copiado. A lista de props do componente é curta e previsível; o que não entra é dado
de uma tela: Botao com tarefa.id dentro é componente com nome próprio, não
reutilizável.
Lista de componente: map com key
Para renderizar lista, o map de componente é o caminho: quando o componente
reutilizável é um item, ele recebe um array e desenha um por elemento:
function Lista({ tarefas }) { return ( <View> {tarefas.map((tarefa) => ( <Item key={tarefa.id} tarefa={tarefa} /> ))} </View> ); }
A chave do item (key) é obrigatória nesse map. Ela diz ao React Native
qual elemento da tela corresponde a qual item do array quando a lista muda de
ordem, de tamanho ou de conteúdo. Sem a chave, dois itens trocam de lugar e a tela
mostra o texto trocado.
A chave é o id, nunca o índice. O índice anda quando a lista anda, e a
key={indice} dá exatamente o bug que a chave existe para evitar.
O exemplo monta a lista de verdade e confirma as duas coisas na mesma sequência.
quantos filhos: 3 é a lista de três tarefas produzindo três elementos, e
cada um ja e o Item desenhado: View, View, View mostra que os três são
View — porque o Item já foi chamado e devolveu o seu View. A lista não
guarda referência ao Item; guarda o que ele devolveu.
Duas linhas leem os dados de cada item:
o texto que cada Item recebeu: [ 'estudar', 'treinar', 'descansar' ] e
o status desenhado: [ 'pendente', 'feita', 'pendente' ]. A segunda é a que
mostra o componente decide desenho a partir da prop: o item 2 é o único com
feita: true, e é o único que desenhou 'feita'.
A linha props de um item: [ 'style', 'children' ] responde a uma pergunta que
o parágrafo deixa de fora. A key não aparece ali: o runtime consome a
chave ao montar o elemento, e ela não vira prop. Por isso a key se escreve no
JSX e não se lê depois.
E o par do fim do exemplo é o id contra o índice:
key por indice: 0, 1, 2 e key por id: 1, 2, 3. As duas listas têm os mesmos
três itens, e as chaves são as mesmas posições — a diferença é o que acontece
quando a lista muda de ordem: com o índice, o item que estava em 2 vira
chave 1 e o runtime procura o elemento errado.
Componente pai e componente filho
O pai é quem tem o estado e monta os filhos; o filho é quem só desenha e
avisa. A regra que organiza o código inteiro do curso:
- quem tem o dado novo é quem tem o
useState; - quem manda o dado para baixo é o pai, por prop;
- quem avisa que algo aconteceu é o filho, chamando a função que recebeu por prop.
// pai: guarda e atualiza <Lista tarefas={tarefas} aoAlternar={() => alternar()} /> // filho: desenha e avisa <Item tarefa={tarefa} aoConcluir={aoAlternar} />
O filho não sabe onde o dado mora e não precisa saber. É por isso que o mesmo
Item serve dentro de Lista, de Detalhe ou de uma tela de busca.
O exemplo demonstra a regra inteira numa linha só:
id avisado: [ 1 ] | antes: false | depois: true. O filho chamou a função que
recebeu por prop; a função do pai empurrou o 1 no array de avisos e devolveu
uma lista nova com o item trocado. O antes: false e o depois: true são o
mesmo item lido em dois momentos.
A linha seguinte é a garantia do padrão:
o array original nao foi mexido: false | o novo e outro objeto: true. O
tarefas original ainda tem a primeira tarefa como feita: false, e o item na
lista nova é outro objeto. O pai não reescreveu nada — ele produziu uma lista
nova, e é essa troca que o React Native entende como mudança.
Esse { ...t, feita: !t.feita } dentro do map é o spread da aula de
desestruturação: o objeto novo nasce com todas as propriedades do antigo e a
feita trocada. Sem o spread, o item novo teria só o campo feita.
Componente com children: o outro lado da composição
Um componente com children não sabe o que vai dentro dele, e é isso que o torna
reutilizável:
<Cartao titulo="Estatísticas"> <Text>12 tarefas</Text> <Text>3 concluídas</Text> </Cartao>
Cartao cuida da borda, do raio e do espaçamento; quem passa o conteúdo decide o
conteúdo. Esse par — componente com estrutura fixa e children para o conteúdo —
é o padrão de composição em tela.
O exemplo sai nas três linhas: titulo do cartao: Estatisticas, `conteudo
vindo de children: 12 tarefas e cartao sem conteudo: vazio`. A segunda é o
conteúdo que veio pela prop, lido em children[1]; a terceira é o padrão do
parágrafo aparecendo: sem prop children, o Cartao devolveu 'vazio'.
O Cartao do exemplo não sabe o que está dentro — recebe o texto '12 tarefas'
como children e o coloca no lugar. Trocar por dois Text não mudaria nada no
componente. É essa ignorância sobre o conteúdo que o torna reutilizável.
Quando não extrair
Três sinais de que não vale a pena extrair:
- Usado uma vez só e sem previsão de reuso — a indireção custa mais que a
duplicação.
- Nome genérico sem comportamento —
Caixaque só embrulha coisa é o
<View> com nome novo.
- Leva e traz o mesmo dado — o pai lê o filho pela prop que ele acabou de
passar, e aí o componente virou um useState mal colocado.
O sinal de que está na hora de extrair é o oposto: a mesma linha de JSX se
repete com diferença de um nome, ou o StyleSheet tem duas entradas com o mesmo
valor de fontSize. Aí a duplicação já custa.
O parágrafo fala de Caixa que só embrulha coisa, e o exemplo da aula anterior
tem exatamente esse componente — Caixa que recebe {...resto} e repassa. Ele
existe ali para mostrar o rest, e o console mostra que ele produz
[ 'onPress', 'testID', 'children' ]: três nomes repassados sem o componente
saber o que são. Isso é reuso de verdade — o componente não conhece nenhuma das
props que repassa.
O sinal que decide é o da lista de props: se a lista do componente tem nome de
tela dentro — tarefa, usuario, pedido — ele já não é reutilizável, e o nome
diz por quê. Item com tarefa prop é componente de linha de tarefa, não
componente de linha.
Props que mudam com o item
O componente de linha costuma receber o item inteiro (tarefa={tarefa}) em vez
de dez props soltas. É mais fácil de estender: quando a tarefa ganhar um campo
novo, o filho já recebe. E o objeto que o filho nunca deve guardar é o que o pai
passa — o filho desenha, o pai decide.
O exemplo passa o objeto inteiro, e a lista de props do View devolvido
confirma que só duas coisas sobreviveram ao desenho: style e children. As
props do Item — tarefa e aoConcluir — não estão ali: o componente leu o
objeto e não o guardou.
O lado do "campo novo" é o alternar do exemplo: o item novo nasce pelo spread
do item antigo, e o t tinha id e texto. Se a tarefa ganhasse um campo
prioridade, ele estaria no item novo sem nenhuma alteração no componente.
Exemplo
// Composicao: o componente reutilizavel, a lista com map e key, e a // regra de quem guarda o dado e quem so desenha. const { View, Text, StyleSheet } = require('react-native'); const styles = StyleSheet.create({ cartao: { padding: 12, backgroundColor: '#ffffff', borderRadius: 8 }, linha: { flexDirection: 'row', alignItems: 'center', padding: 12 }, titulo: { fontSize: 16, color: '#222222' }, }); const tarefas = [ { id: 1, texto: 'estudar', feita: false }, { id: 2, texto: 'treinar', feita: true }, { id: 3, texto: 'descansar', feita: false }, ]; /* o filho: so desenha o que veio por prop */ function Item({ tarefa, aoConcluir }) { return ( <View style={styles.linha}> <Text style={styles.titulo}>{tarefa.texto}</Text> <Text>{tarefa.feita ? 'feita' : 'pendente'}</Text> </View> ); } /* o pai: recebe a lista e monta um Item por elemento */ function Lista({ tarefas, aoAlternar }) { return ( <View> {tarefas.map((tarefa) => ( <Item key={tarefa.id} tarefa={tarefa} aoConcluir={aoAlternar} /> ))} </View> ); } const lista = Lista({ tarefas, aoAlternar: () => {} }); const filhos = lista.props.children; console.log('quantos filhos:', filhos.length); console.log('cada um ja e o Item desenhado:', filhos.map((f) => f.type).join(', ')); console.log('o texto que cada Item recebeu:', filhos.map((f) => f.props.children[0].props.children)); console.log('o status desenhado:', filhos.map((f) => f.props.children[1].props.children)); /* a key nao vira prop: o runtime consome a chave do elemento ao montar. Por isso ela se escreve no JSX, e nao da para ler depois. */ console.log('props de um item:', Object.keys(filhos[0].props)); /* a key e o id, nunca o indice: o indice anda quando a lista anda */ const porIndice = tarefas.map((tarefa, indice) => ({ id: indice, texto: tarefa.texto })); console.log('key por indice:', porIndice.map((t) => t.id).join(', ')); console.log('key por id:', tarefas.map((t) => t.id).join(', ')); /* o componente com children: estrutura fixa, conteudo livre */ function Cartao({ titulo, children = 'vazio' }) { return ( <View style={styles.cartao}> <Text style={styles.titulo}>{titulo}</Text> {children} </View> ); } const cartao = Cartao({ titulo: 'Estatisticas', children: '12 tarefas' }); console.log('titulo do cartao:', cartao.props.children[0].props.children); console.log('conteudo vindo de children:', cartao.props.children[1]); console.log('cartao sem conteudo:', Cartao({ titulo: 'Vazio' }).props.children[1]); /* o pai guarda o dado novo e o filho so avisa */ let listaAtual = tarefas; const avisos = []; function alternar(id) { avisos.push(id); listaAtual = listaAtual.map((t) => (t.id === id ? { ...t, feita: !t.feita } : t)); return listaAtual; } function ItemAvisa({ tarefa, aoConcluir }) { return <View><Text>{tarefa.texto}</Text></View>; } const marcado = ItemAvisa({ tarefa: listaAtual[0], aoConcluir: () => alternar(listaAtual[0].id) }); marcado; alternar(1); console.log('id avisado:', avisos, '| antes:', tarefas[0].feita, '| depois:', listaAtual[0].feita); console.log('o array original nao foi mexido:', tarefas[0].feita, '| o novo e outro objeto:', listaAtual[0] !== tarefas[0]); /* o mesmo componente em duas telas: e a reutilizacao */ const tela1 = Lista({ tarefas: listaAtual }); const tela2 = Lista({ tarefas: tarefas.slice(0, 1) }); console.log('mesmo componente, duas telas:', tela1.props.children.length, 'e', tela2.props.children.length, 'itens');
Saída real
quantos filhos: 3 cada um ja e o Item desenhado: View, View, View o texto que cada Item recebeu: [ 'estudar', 'treinar', 'descansar' ] o status desenhado: [ 'pendente', 'feita', 'pendente' ] props de um item: [ 'style', 'children' ] key por indice: 0, 1, 2 key por id: 1, 2, 3 titulo do cartao: Estatisticas conteudo vindo de children: 12 tarefas cartao sem conteudo: vazio id avisado: [ 1 ] | antes: false | depois: true o array original nao foi mexido: false | o novo e outro objeto: true mesmo componente, duas telas: 3 e 1 itens