Estado e formulários no React Native
Controle TextInput, valide dados e atualize uma lista com useReducer no React Native, entendendo estado sem depender de tentativa e erro.
Estado é a memória de um componente entre renderizações. Num formulário React
Native, o TextInput mostra um valor, onChangeText envia cada alteração e uma
função de atualização guarda a nova versão. Nesta lição, você liga esse ciclo a
uma lista de hábitos com validação e mensagens que a pessoa consegue perceber.
Continue no projeto das lições anteriores. Se props e componentes ainda estão misturados, revise componentes e estilos antes de adicionar memória à tela.
A comanda que acompanha o pedido
Imagine a comanda de uma lanchonete. Conforme alguém pede, troca ou remove um item, a comanda registra a situação atual. A cozinha não tenta adivinhar o que foi dito cinco minutos atrás; ela trabalha com o registro mais recente.
No mapa técnico, a comanda é o state, cada pedido de mudança é uma ação e a
tela renderizada é a leitura atual da comanda. useState resolve memórias
independentes; useReducer centraliza mudanças relacionadas numa função que
recebe estado e ação e devolve o próximo estado.
O limite é importante: estado React não é armazenamento permanente. Se o app for encerrado, a memória pode desaparecer. Tecnicamente, o state vive na árvore de componentes; persistência no aparelho será adicionada na lição de APIs e armazenamento.
No guia de React Native, essa memória aparece entre a
lógica React e os controles nativos. Essa posição explica por que useState
não escreve diretamente no campo: ele solicita outra renderização, e então o
novo value chega ao TextInput. Quando esse ciclo está claro, você investiga
qual evento produziu qual próxima versão dos dados.
Controle o TextInput pelos dois lados
Um campo controlado recebe value do estado e informa mudanças por
onChangeText. Crie src/components/habit-form.tsx:
import { useState } from 'react';
import { Button, StyleSheet, Text, TextInput, View } from 'react-native';
type HabitFormProps = {
onSubmit: (title: string) => void;
};
export function HabitForm({ onSubmit }: HabitFormProps) {
const [title, setTitle] = useState('');
const [error, setError] = useState('');
function handleSubmit() {
const normalizedTitle = title.trim();
if (!normalizedTitle) {
setError('Digite um hábito antes de adicionar.');
return;
}
onSubmit(normalizedTitle);
setTitle('');
setError('');
}
return (
<View style={styles.form}>
<TextInput
accessibilityLabel="Nome do novo hábito"
onChangeText={setTitle}
onSubmitEditing={handleSubmit}
placeholder="Ex.: caminhar por 15 minutos"
returnKeyType="done"
value={title}
/>
{error ? <Text accessibilityLiveRegion="polite">{error}</Text> : null}
<Button onPress={handleSubmit} title="Adicionar hábito" />
</View>
);
}Quando a pessoa digita Ler, o componente não lê o DOM como faria uma página
web. O TextInput chama onChangeText('Ler'), setTitle agenda a atualização e
a próxima renderização recebe value="Ler". Essa volta completa é o que torna
o campo controlado.
Complete um estilo simples:
const styles = StyleSheet.create({
form: {
backgroundColor: '#ffffff',
borderRadius: 14,
gap: 10,
padding: 16,
},
});Em um app final, dê também estilo ao campo e use uma área de pressão alinhada à
identidade visual. O Button nativo é útil aqui porque mantém a prática focada
no fluxo de dados.
Modele ações antes de espalhar alterações
Crie src/lib/habits.ts. O estado é uma lista e cada ação carrega apenas os
dados necessários:
import type { Habit } from '@/types/habit';
export type HabitAction =
| { type: 'added'; habit: Habit }
| { type: 'toggled'; id: string }
| { type: 'loaded'; habits: Habit[] };
export function habitsReducer(state: Habit[], action: HabitAction): Habit[] {
switch (action.type) {
case 'added':
return [action.habit, ...state];
case 'toggled':
return state.map((habit) =>
habit.id === action.id ? { ...habit, done: !habit.done } : habit,
);
case 'loaded':
return action.habits;
}
}O termo técnico para HabitAction é discriminated union: type discrimina
qual formato está presente. Dentro de case 'toggled', TypeScript sabe que há
um id; dentro de added, sabe que há um habit.
O reducer não altera o array recebido. map, spread e o novo array preservam
imutabilidade, que aqui significa criar referências novas para o trecho que
mudou. Assim React consegue perceber a atualização e o histórico lógico não é
modificado pelas costas.
Ligue formulário e reducer na tela
Na tela inicial, inicialize a lista e transforme callbacks em ações:
import { useReducer } from 'react';
import { HabitForm } from '@/components/habit-form';
import { habitsReducer } from '@/lib/habits';
export default function HomeScreen() {
const [habits, dispatch] = useReducer(habitsReducer, []);
function addHabit(title: string) {
dispatch({
type: 'added',
habit: { id: String(Date.now()), title, done: false },
});
}
return (
<>
<HabitForm onSubmit={addHabit} />
{/* a lista usará habits */}
</>
);
}dispatch não altera imediatamente a constante habits daquela execução. Ele
solicita outra renderização com o estado resultante. Por isso, imprimir
habits.length logo depois de dispatch pode mostrar o valor anterior. A
função ainda enxerga o snapshot da renderização em que foi criada.
Esse snapshot mantém todas as leituras daquela execução coerentes. Se duas ações dependem do valor anterior, descreva a transição no reducer em vez de guardar uma variável mutável fora do React. Estado externo pode mudar, mas não agenda renderização nem respeita a vida do componente; o sintoma costuma ser uma tela atrasada em relação ao dado.
Para ids locais, Date.now() atende a esta prática, mas pode colidir em eventos
muito próximos. Num produto, prefira o id vindo do servidor ou uma estratégia
de UUID adequada ao runtime.
Não use estado para uma informação calculável
Quantidade concluída depende apenas de habits; não precisa de outro
useState. Calcule durante a renderização:
const completed = habits.filter((habit) => habit.done).length;
<Text>
{completed} de {habits.length} concluídos
</Text>;Guardar também completed criaria duas comandas para o mesmo pedido. Se uma
ação atualizasse a lista e esquecesse o contador, a interface ficaria
inconsistente. Tecnicamente, valores derivados devem ser calculados da fonte de
verdade, a menos que exista uma razão medida para memorizar o cálculo.
Abra espaço para o teclado sem adivinhar plataforma
O teclado virtual pode cobrir o formulário. Envolva a tela e escolha o comportamento pela plataforma:
import { KeyboardAvoidingView, Platform } from 'react-native';
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={{ flex: 1 }}
>
{/* formulário e lista */}
</KeyboardAvoidingView>;KeyboardAvoidingView ajusta altura, posição ou padding em resposta ao teclado.
O resultado varia com layout e plataforma; teste em telas pequenas e com fonte
aumentada. O componente não elimina a necessidade de rolagem quando o conteúdo
é maior que a área disponível.
Reproduza o caso vazio e trate como regra
Sem trim, um título composto por espaços entra na lista. Isole a normalização
para que ela também possa ser testada:
export function normalizeHabitTitle(value: string): string {
const title = value.trim();
if (!title) throw new Error('O título do hábito está vazio.');
return title;
}O teste reproduz o caminho inválido e o válido:
test('recusa um título vazio', () => {
expect(() => normalizeHabitTitle(' ')).toThrow(
'O título do hábito está vazio.',
);
});
test('remove espaços externos', () => {
expect(normalizeHabitTitle(' Beber água ')).toBe('Beber água');
});No projeto verificado, npm test -- --ci registrou:
No formulário desta lição, preferimos mostrar a mensagem sem lançar exceção, porque entrada vazia é uma situação prevista da interface. A função que lança serve para provar e proteger uma fronteira de domínio. Erro esperado deve virar feedback; erro inesperado deve ser investigado.
Missão: faça a comanda corrigir um item
Acrescente uma ação renamed com id e title. No reducer, devolva uma cópia
do hábito correspondente, sem mutar o objeto original. Escreva o teste antes de
ligar a uma interface de edição.
Entrada: [{ id: 'agua', title: 'Água', done: false }]
Ação: { type: 'renamed', id: 'agua', title: 'Beber 2 copos' }
Saída esperada: [{ id: 'agua', title: 'Beber 2 copos', done: false }]Confira três coisas: o título mudou, done foi preservado e o array de saída
não é a mesma referência do array de entrada. Na próxima lição, esses hábitos
ganham uma lista virtualizada e cada item pode abrir sua própria rota em
listas e navegação.
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, Expo SDK 57.0.15, React Native 0.86.2, React 19.2.3, TypeScript 6.0.3 e Jest 29.7.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React — State: A Component’s Memory — react.dev
- React — Extracting State Logic into a Reducer — react.dev
- React Native — TextInput — reactnative.dev
- React Native — KeyboardAvoidingView — reactnative.dev


