Modelo mental: estado, props e renderização
Em React Native, a UI é uma função do estado e das props. Sempre que um estado muda (via setState de um hook) ou quando o componente pai renderiza e passa novas props, o componente pode renderizar novamente. Renderizar significa executar a função do componente para produzir uma nova árvore de elementos; depois, o React calcula o que mudou e aplica as atualizações necessárias na tela.
O objetivo de performance não é “evitar render a qualquer custo”, e sim evitar trabalho desnecessário: cálculos pesados repetidos, recriação de funções/objetos que causam re-render em filhos, efeitos disparando em excesso, e listas re-renderizando itens sem necessidade.
Como identificar o que causa render
- Estado local mudou:
setXfoi chamado. - Props mudaram: o pai passou um novo valor (mesmo que “igual”, mas com nova referência).
- Context mudou (quando usado).
- Strict Mode em desenvolvimento pode executar render/effects mais de uma vez para ajudar a detectar problemas (não ocorre assim em produção).
useState: estado local e atualizações previsíveis
useState guarda um valor entre renders. Ao chamar o setter, o React agenda uma atualização e o componente renderiza novamente com o novo estado.
Padrões essenciais
- Atualização funcional quando o novo estado depende do anterior: evita bugs por closures antigas.
- Estado mínimo: armazene o que é necessário; derive o restante com cálculos (e use memoização se for caro).
- Evite estado duplicado: se algo pode ser derivado de props/estado existente, não replique.
Exemplo prático: contador com atualização funcional
import React, { useState } from 'react';import { View, Text, Button } from 'react-native';export function Counter() { const [count, setCount] = useState(0); return ( <View> <Text>Count: {count}</Text> <Button title="+1" onPress={() => setCount(c => c + 1)} /> <Button title="Reset" onPress={() => setCount(0)} /> </View> );}Use a forma setCount(c => c + 1) quando houver múltiplas atualizações em sequência ou eventos rápidos, garantindo que o cálculo use o valor mais recente.
useEffect: ciclo de vida com efeitos e cleanup
useEffect executa efeitos colaterais após o render: buscar dados, assinar eventos, iniciar timers, sincronizar com APIs externas. Ele roda depois que a UI foi calculada, e pode retornar uma função de cleanup para desfazer o efeito (equivalente a “desmontar” ou “antes de rodar novamente”).
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Dependências corretas: regra prática
O array de dependências deve conter tudo que o efeito usa e que pode mudar entre renders (props, estados, funções/valores definidos no componente). Se você omite dependências, corre risco de usar valores antigos (stale closure). Se você inclui dependências demais (como objetos recriados a cada render), o efeito pode rodar em excesso.
Padrões comuns de useEffect
| Padrão | Dependências | Quando usar |
|---|---|---|
| Rodar uma vez (montagem) | [] | Inicialização, uma busca inicial (com cuidado), setup de listeners |
| Rodar quando algo muda | [id] | Reagir a mudanças de props/estado específicos |
| Com cleanup | [deps] + return () => ... | Timers, subscriptions, listeners, abort de requests |
Exemplo prático: timer com cleanup
import React, { useEffect, useState } from 'react';import { Text } from 'react-native';export function Stopwatch() { const [seconds, setSeconds] = useState(0); useEffect(() => { const id = setInterval(() => { setSeconds(s => s + 1); }, 1000); return () => clearInterval(id); }, []); return <Text>Seconds: {seconds}</Text>;}Sem o cleanup, o interval continuaria rodando mesmo após o componente sair da tela, causando vazamento de recursos e atualizações em componente desmontado.
Exemplo prático: busca com loading/erro e cancelamento
Um padrão robusto para requisições é controlar loading, error e data, e cancelar a requisição no cleanup para evitar atualizar estado após desmontagem.
import React, { useEffect, useState } from 'react';import { View, Text, ActivityIndicator, Button } from 'react-native';async function fetchUser(userId, signal) { const res = await fetch(`https://example.com/users/${userId}`, { signal }); if (!res.ok) throw new Error('Falha ao carregar usuário'); return res.json();}export function UserProfile({ userId }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const load = async (signal) => { setLoading(true); setError(null); try { const user = await fetchUser(userId, signal); setData(user); } catch (e) { if (e.name !== 'AbortError') setError(e); } finally { setLoading(false); } }; useEffect(() => { const controller = new AbortController(); load(controller.signal); return () => controller.abort(); }, [userId]); if (loading) return <ActivityIndicator />; if (error) { return ( <View> <Text>Erro: {String(error.message || error)}</Text> <Button title="Tentar novamente" onPress={() => { const controller = new AbortController(); load(controller.signal); }} /> </View> ); } if (!data) return <Text>Sem dados</Text>; return <Text>Olá, {data.name}</Text>;}Observações importantes: (1) o efeito depende de userId, então refaz a busca quando ele muda; (2) o AbortController evita setState após unmount; (3) o padrão try/catch/finally garante que loading seja desligado.
useMemo: memoização de valores (não de renders)
useMemo memoriza o resultado de um cálculo e só recalcula quando as dependências mudam. Ele não impede o componente de renderizar; ele evita recalcular algo caro em todo render.
Quando faz sentido
- Filtragem/ordenação pesada de listas antes de renderizar.
- Construção de estruturas (maps/dicionários) usadas em várias partes do render.
- Evitar criar objetos/arrays novos que são passados como props e causam re-render em filhos memoizados.
Exemplo prático: filtragem e ordenação
import React, { useMemo, useState } from 'react';import { View, TextInput, FlatList, Text } from 'react-native';export function SearchableList({ items }) { const [query, setQuery] = useState(''); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); const base = q ? items.filter(i => i.name.toLowerCase().includes(q)) : items; return [...base].sort((a, b) => a.name.localeCompare(b.name)); }, [items, query]); return ( <View> <TextInput value={query} onChangeText={setQuery} placeholder="Buscar..." /> <FlatList data={filtered} keyExtractor={(item) => String(item.id)} renderItem={({ item }) => <Text>{item.name}</Text>} /> </View> );}Sem useMemo, a filtragem/ordenação rodaria a cada tecla e também em qualquer render provocado por outros estados, mesmo que items e query não tenham mudado.
useCallback: memoização de funções para props estáveis
useCallback memoriza a referência de uma função. Isso é útil quando você passa callbacks para componentes filhos que usam React.memo (ou quando o callback é dependência de um useEffect e você quer controlar quando ele muda).
Exemplo prático: evitando re-render de item de lista
Imagine um item de lista memoizado. Se o pai recria onPress a cada render, o item recebe uma nova prop e renderiza de novo.
import React, { useCallback, useState, memo } from 'react';import { FlatList, Text, Pressable, View } from 'react-native';const Row = memo(function Row({ item, onPress }) { console.log('render Row', item.id); return ( <Pressable onPress={() => onPress(item.id)}> <Text>{item.name}</Text> </Pressable> );});export function ListScreen({ items }) { const [selectedId, setSelectedId] = useState(null); const handlePress = useCallback((id) => { setSelectedId(id); }, []); return ( <View> <Text>Selecionado: {selectedId ?? 'nenhum'}</Text> <FlatList data={items} keyExtractor={(i) => String(i.id)} renderItem={({ item }) => ( <Row item={item} onPress={handlePress} /> )} /> </View> );}Com useCallback, handlePress mantém a mesma referência entre renders (enquanto dependências não mudarem), permitindo que Row permaneça estável quando apenas selectedId muda.
Armadilha comum: dependências incorretas
Se o callback usa valores que mudam, eles devem estar nas dependências. Caso contrário, o callback pode “enxergar” valores antigos.
// Exemplo: se você usa `selectedId` dentro do callback, inclua-o nas depsconst handlePress = useCallback((id) => { if (id === selectedId) return; setSelectedId(id);}, [selectedId]);useRef: estado mutável que não dispara render
useRef guarda um objeto com a propriedade current que persiste entre renders. Alterar ref.current não causa re-render. É ideal para: (1) guardar IDs de timers, (2) manter referência a instâncias, (3) armazenar o “último valor” para comparação, (4) evitar condições de corrida em efeitos.
Exemplo prático: evitar setState após unmount (flag)
import React, { useEffect, useRef, useState } from 'react';import { Text } from 'react-native';export function SafeLoader() { const [value, setValue] = useState(null); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; (async () => { await new Promise(r => setTimeout(r, 800)); if (mountedRef.current) setValue('carregado'); })(); return () => { mountedRef.current = false; }; }, []); return <Text>{value ?? 'carregando...'}</Text>;}Esse padrão é útil quando você não controla a API para cancelamento. Quando possível, prefira cancelamento real (ex.: AbortController).
Exemplo prático: debouncing com timer em ref
import React, { useEffect, useRef, useState } from 'react';import { TextInput, Text, View } from 'react-native';export function DebouncedSearch() { const [query, setQuery] = useState(''); const [debounced, setDebounced] = useState(''); const timerRef = useRef(null); useEffect(() => { if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(() => { setDebounced(query); }, 400); return () => { if (timerRef.current) clearTimeout(timerRef.current); }; }, [query]); return ( <View> <TextInput value={query} onChangeText={setQuery} placeholder="Digite..." /> <Text>Buscando por: {debounced}</Text> </View> );}React.memo: memoização de componente (evitar re-render por props iguais)
React.memo memoriza o resultado de render de um componente funcional e evita re-render quando as props são consideradas iguais (comparação rasa por padrão). Ele é mais efetivo quando:
- O componente renderiza frequentemente com as mesmas props.
- O componente é relativamente “caro” (muito layout, imagens, cálculos).
- As props são estáveis (funções com
useCallback, objetos comuseMemo).
Exemplo prático: componente de card memoizado
import React, { memo } from 'react';import { View, Text } from 'react-native';export const UserCard = memo(function UserCard({ name, subtitle }) { console.log('render UserCard', name); return ( <View> <Text>{name}</Text> <Text>{subtitle}</Text> </View> );});Se o pai renderiza por causa de um estado que não afeta UserCard, e as props name/subtitle não mudam, o card não renderiza novamente.
Comparação customizada (use com cuidado)
Se você passa objetos grandes e quer controlar a comparação, pode fornecer uma função. Use apenas quando a comparação rasa não é suficiente e você tem certeza do ganho.
export const Row = memo( function Row({ item }) { return <Text>{item.name}</Text>; }, (prev, next) => prev.item.id === next.item.id && prev.item.name === next.item.name);Como hooks afetam performance e comportamento de renderização
Checklist de sintomas e correções
| Sintoma | Causa provável | Correção típica |
|---|---|---|
| Filho memoizado renderiza sempre | Props mudam por referência (funções/objetos recriados) | useCallback para funções, useMemo para objetos/arrays |
| Efeito roda em loop | Dependência muda a cada render (objeto/função instável) ou efeito atualiza estado que altera dependência | Estabilizar dependências, revisar lógica, separar efeitos |
| Lista lenta ao digitar | Filtragem/ordenação pesada em todo render | useMemo para computação, otimizar FlatList e itens memoizados |
| Warnings de setState após unmount | Request/timer não cancelado | Cleanup com abort/clearInterval/flag via useRef |
Criação de hooks reutilizáveis para encapsular lógica
Custom hooks permitem extrair lógica de estado/efeitos e reutilizar em várias telas/componentes. Um bom hook:
- Tem uma API pequena e clara (retorna dados e ações).
- Encapsula detalhes de loading/erro/cancelamento.
- Evita expor implementações internas (ex.: controllers, refs).
Passo a passo: criando um hook useAsync
Objetivo: executar uma função assíncrona, controlar loading, error e data, e permitir re-execução.
1) Defina o estado e a função de execução
import { useCallback, useRef, useState } from 'react';export function useAsync(asyncFn) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const callIdRef = useRef(0); const run = useCallback(async (...args) => { const callId = ++callIdRef.current; setLoading(true); setError(null); try { const result = await asyncFn(...args); if (callId === callIdRef.current) setData(result); return result; } catch (e) { if (callId === callIdRef.current) setError(e); throw e; } finally { if (callId === callIdRef.current) setLoading(false); } }, [asyncFn]); return { data, loading, error, run, setData };}O callIdRef ajuda a evitar condições de corrida: se você disparar duas chamadas, apenas a última atualiza o estado.
2) Use o hook em um componente
import React, { useEffect } from 'react';import { View, Text, Button, ActivityIndicator } from 'react-native';import { useAsync } from './useAsync';async function fetchProducts() { const res = await fetch('https://example.com/products'); if (!res.ok) throw new Error('Erro ao buscar produtos'); return res.json();}export function Products() { const { data, loading, error, run } = useAsync(fetchProducts); useEffect(() => { run(); }, [run]); if (loading) return <ActivityIndicator />; if (error) { return ( <View> <Text>Erro: {String(error.message || error)}</Text> <Button title="Recarregar" onPress={() => run()} /> </View> ); } return ( <View> <Text>Itens: {data?.length ?? 0}</Text> </View> );}Note que run é estável por causa do useCallback, então o useEffect não entra em loop.
Debugging de renderizações e efeitos
Logs estratégicos (sem poluir)
Use logs para responder: “quem renderizou?”, “por quê?”, “com quais props/estado?”. Evite logar objetos enormes ou em loops de lista.
1) Log de render do componente
export function Profile({ userId }) { console.log('render Profile', { userId }); // ...}2) Log de mudanças específicas com useEffect
import React, { useEffect } from 'react';export function DebugExample({ query, page }) { useEffect(() => { console.log('query mudou', query); }, [query]); useEffect(() => { console.log('page mudou', page); }, [page]); return null;}3) Descobrir props instáveis
Quando um filho memoizado renderiza sem necessidade, suspeite de props por referência (funções/objetos). Um padrão simples é logar igualdade referencial:
import React, { useEffect, useRef } from 'react';export function WhyChanged({ options }) { const prevRef = useRef(options); useEffect(() => { const same = prevRef.current === options; console.log('options mesma referência?', same); prevRef.current = options; }, [options]); return null;}React DevTools (quando aplicável)
Com React DevTools, você consegue inspecionar a árvore de componentes e observar re-renders. Em cenários de performance, procure por:
- Componentes renderizando em cascata quando um estado muda.
- Props que mudam a cada render (especialmente objetos inline e callbacks).
- Componentes de lista re-renderizando itens fora do necessário.
Uma prática útil é combinar DevTools com logs em componentes críticos (itens de lista, headers, barras de busca) para confirmar o impacto de cada mudança.
Identificando e corrigindo renders desnecessários: roteiro prático
Passo 1: reproduza e meça com um ponto de observação
// No componente que você suspeita estar renderizando demaisconsole.log('render SearchHeader');Passo 2: isole a causa
- Comente temporariamente partes do JSX para ver se o render em excesso vem de um filho específico.
- Verifique se o estado que muda está no componente certo (às vezes vale mover estado para baixo na árvore).
Passo 3: estabilize props
Evite criar objetos inline quando eles são props de componentes memoizados:
// Evite: cria um novo objeto a cada render<Child style={{ padding: 12 }} />// Prefira: memoize ou use StyleSheet/const fora do componenteconst childStyle = { padding: 12 };<Child style={childStyle} />Para callbacks:
// Evite: nova função a cada render<Child onPress={() => doSomething(id)} />// Prefira: useCallback e passe parâmetros no filho, ou crie factory memoizadaconst onPress = useCallback(() => doSomething(id), [id, doSomething]);<Child onPress={onPress} />Passo 4: aplique memoização onde há ganho real
useMemopara computação cara ou para estabilizar objetos/arrays passados como props.useCallbackpara estabilizar funções passadas para filhos memoizados.React.memopara componentes que recebem props estáveis e renderizam com frequência.
Passo 5: revise efeitos para evitar trabalho duplicado
- Garanta cleanup de subscriptions/timers.
- Evite efeitos que atualizam estado que está nas dependências sem necessidade (loop).
- Separe efeitos por responsabilidade (um para fetch, outro para analytics/log, etc.).