Estado e ciclo de vida em React Native: hooks essenciais e comportamento de renderização

Capítulo 6

Tempo estimado de leitura: 14 minutos

+ Exercício

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: setX foi 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”).

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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ãoDependênciasQuando 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 com useMemo).

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

SintomaCausa provávelCorreção típica
Filho memoizado renderiza sempreProps mudam por referência (funções/objetos recriados)useCallback para funções, useMemo para objetos/arrays
Efeito roda em loopDependência muda a cada render (objeto/função instável) ou efeito atualiza estado que altera dependênciaEstabilizar dependências, revisar lógica, separar efeitos
Lista lenta ao digitarFiltragem/ordenação pesada em todo renderuseMemo para computação, otimizar FlatList e itens memoizados
Warnings de setState após unmountRequest/timer não canceladoCleanup 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

  • useMemo para computação cara ou para estabilizar objetos/arrays passados como props.
  • useCallback para estabilizar funções passadas para filhos memoizados.
  • React.memo para 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.).

Agora responda o exercício sobre o conteúdo:

Em um componente React Native que faz uma requisição dentro de useEffect, qual abordagem reduz o risco de atualizar estado após o componente desmontar e evita trabalho desnecessário?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O useEffect pode retornar um cleanup. Cancelar a requisie7e3o (ex.: AbortController) ou usar uma flag em useRef evita vazamentos e o erro de chamar setState apf3s o componente desmontar.

Próximo capitúlo

Gerenciamento de estado com Context API em React Native (padrão para apps pequenos e médios)

Arrow Right Icon
Capa do Ebook gratuito React Native Essencial: criando apps completos com JavaScript e boas práticas
38%

React Native Essencial: criando apps completos com JavaScript e boas práticas

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.