Qualidade como parte do fluxo de desenvolvimento
Em apps React Native, qualidade e manutenção dependem de três pilares no dia a dia: padronização automática (lint e formatação), testes (regras de negócio e interações de UI) e acessibilidade (componentes utilizáveis por mais pessoas). A ideia é reduzir decisões repetitivas, evitar regressões e tornar o código previsível para quem mantém o projeto.
ESLint + Prettier no React Native
O que cada ferramenta faz
- ESLint: encontra problemas (erros, más práticas, imports inconsistentes, hooks mal usados) e pode corrigir parte deles automaticamente.
- Prettier: formata o código (espaços, quebras, aspas, trailing commas) de forma determinística.
Boa prática: deixar o Prettier cuidar de formatação e o ESLint cuidar de regras de código. Para evitar conflito, usamos configurações que desativam regras do ESLint que “brigam” com o Prettier.
Passo a passo: instalação e configuração
1) Instale dependências:
npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier2) Adicione plugins úteis para React Native e qualidade:
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import eslint-plugin-unused-imports eslint-plugin-jest3) Crie (ou ajuste) o arquivo .eslintrc.js com um conjunto de regras pragmático:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
module.exports = { root: true, env: { es2021: true, node: true, jest: true }, parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', 'react', 'react-hooks', 'import', 'unused-imports', 'prettier' ], extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript', 'plugin:prettier/recommended', 'prettier' ], settings: { react: { version: 'detect' }, 'import/resolver': { typescript: true } }, rules: { 'prettier/prettier': 'error', 'react/react-in-jsx-scope': 'off', '@typescript-eslint/no-unused-vars': 'off', 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ 'warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' } ], 'import/order': [ 'error', { 'newlines-between': 'always', alphabetize: { order: 'asc', caseInsensitive: true }, groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'type'], pathGroups: [ { pattern: 'react', group: 'external', position: 'before' }, { pattern: 'react-native', group: 'external', position: 'before' }, { pattern: '@/**', group: 'internal' } ], pathGroupsExcludedImportTypes: ['react', 'react-native'] } ], 'import/no-duplicates': 'error', 'import/newline-after-import': 'error' }, ignorePatterns: ['node_modules/', 'android/', 'ios/', 'dist/', 'build/']};4) Crie o arquivo .prettierrc:
{ "singleQuote": true, "trailingComma": "all", "printWidth": 100, "semi": true}5) Garanta que o Prettier não formate pastas geradas com um .prettierignore:
node_modulesandroidiosdistbuildcoveragePadronizando imports (ordem e limpeza)
Com as regras acima, você ganha:
- Ordenação consistente de imports (externos, internos, relativos).
- Quebra de linha obrigatória após bloco de imports.
- Remoção automática de imports não usados (via
unused-imports).
Exemplo do padrão esperado:
import React, { useMemo } from 'react';import { Pressable, Text, View } from 'react-native';import { formatCurrency } from '@/utils/formatCurrency';import type { Product } from '@/types/product';Scripts úteis no package.json
Organize scripts para rodar localmente e no CI:
{ "scripts": { "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "format": "prettier --check .", "format:write": "prettier --write .", "test": "jest", "test:watch": "jest --watch", "test:ci": "jest --ci --runInBand", "typecheck": "tsc --noEmit" }}Dica prática: em PRs, é comum exigir pelo menos lint, format, typecheck e test:ci passando.
Testes com Jest e Testing Library (React Native)
O que testar: regra de negócio vs. interação de UI
- Regras de negócio: funções puras (cálculo, validação, transformação). São rápidas, estáveis e dão alto retorno.
- Componentes: renderização e interações (toque, mudança de texto, estados). Foque no comportamento observável pelo usuário.
Passo a passo: setup básico
1) Instale dependências:
npm i -D jest @types/jest react-test-renderer @testing-library/react-native @testing-library/jest-native2) Crie jest.config.js (exemplo genérico para React Native):
module.exports = { preset: 'react-native', setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], testMatch: ['**/?(*.)+(spec|test).(ts|tsx|js)'], transformIgnorePatterns: [ 'node_modules/(?!(react-native|@react-native|@react-navigation)/)' ], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/index.{ts,tsx}', '!src/**/*.d.ts' ]};3) Crie jest.setup.js:
import '@testing-library/jest-native/extend-expect';Exemplo 1: teste de regra de negócio (função pura)
Suponha uma regra: aplicar desconto progressivo e impedir total negativo.
// src/domain/pricing/calcTotal.tsimport type { CartItem } from './types';export function calcTotal(items: CartItem[], couponPercent: number) { const subtotal = items.reduce((acc, item) => acc + item.price * item.qty, 0); const discount = subtotal * (Math.max(0, Math.min(couponPercent, 100)) / 100); const total = subtotal - discount; return Math.max(0, Number(total.toFixed(2)));}Teste unitário:
// src/domain/pricing/calcTotal.test.tsimport { calcTotal } from './calcTotal';describe('calcTotal', () => { it('soma itens e aplica cupom', () => { const total = calcTotal( [ { price: 10, qty: 2 }, { price: 5, qty: 1 } ], 10 ); expect(total).toBe(22.5); }); it('limita cupom entre 0 e 100', () => { expect(calcTotal([{ price: 10, qty: 1 }], 999)).toBe(0); expect(calcTotal([{ price: 10, qty: 1 }], -10)).toBe(10); });});Esse tipo de teste é estável porque não depende de UI, navegação ou timers.
Exemplo 2: teste de componente com interação (UI)
Componente simples: contador com botão de incremento e estado desabilitado ao atingir limite.
// src/components/Counter.tsximport React, { useMemo, useState } from 'react';import { Pressable, Text, View } from 'react-native';type Props = { initial?: number; max?: number;};export function Counter({ initial = 0, max = 5 }: Props) { const [value, setValue] = useState(initial); const canInc = useMemo(() => value < max, [value, max]); return ( <View> <Text accessibilityRole="header">Contador</Text> <Text testID="counter-value">{value}</Text> <Pressable accessibilityRole="button" accessibilityLabel="Incrementar" onPress={() => canInc && setValue((v) => v + 1)} disabled={!canInc} > <Text>Somar</Text> </Pressable> </View> );}Teste com Testing Library:
// src/components/Counter.test.tsximport React from 'react';import { render, fireEvent } from '@testing-library/react-native';import { Counter } from './Counter';describe('Counter', () => { it('incrementa ao tocar no botão', () => { const { getByA11yLabel, getByTestId } = render(<Counter initial={0} max={2} />); fireEvent.press(getByA11yLabel('Incrementar')); expect(getByTestId('counter-value')).toHaveTextContent('1'); }); it('desabilita ao atingir o máximo', () => { const { getByA11yLabel } = render(<Counter initial={2} max={2} />); const button = getByA11yLabel('Incrementar'); expect(button).toBeDisabled(); });});Boas práticas para testes de UI:
- Prefira queries por acessibilidade (
getByA11yLabel,getByRolequando disponível) em vez detestIDpara elementos interativos. - Teste o comportamento (texto, estado disabled, chamadas de callback), não detalhes internos (states, hooks).
- Evite snapshots grandes; use asserts específicos.
Mocks comuns (quando necessário)
Em componentes que dependem de módulos nativos (ex.: armazenamento, permissões), use mocks para isolar o teste. Exemplo simples de mock manual:
// __mocks__/react-native/Libraries/Animated/NativeAnimatedHelper.jsmodule.exports = {};Use mocks com parcimônia: quanto mais mock, menor a confiança no comportamento real.
Acessibilidade (a11y) no React Native
O que verificar
- Labels: elementos clicáveis precisam de
accessibilityLabelclaro. - Roles: use
accessibilityRole(button, header, image, link, switch, etc.). - Tamanho de toque: área mínima confortável (prática comum: ~44x44pt). Em RN, complemente com
hitSlop. - Contraste: texto e ícones precisam ter contraste suficiente com o fundo (verificação manual + tokens de cor consistentes).
- Estado e feedback:
disabled,selected,checkeddevem refletir o estado real. - Ordem de leitura: hierarquia e agrupamento coerentes; cuidado com elementos fora de ordem visual.
Práticas recomendadas em componentes
1) Botões e ícones clicáveis
Quando o botão é apenas um ícone, o label é obrigatório. Garanta também área de toque adequada:
import React from 'react';import { Pressable } from 'react-native';import { CloseIcon } from '@/components/icons/CloseIcon';export function CloseButton({ onPress }: { onPress: () => void }) { return ( <Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel="Fechar" hitSlop={12} > <CloseIcon /> </Pressable> );}2) Imagens
Se a imagem for decorativa, o ideal é não “poluir” leitores de tela. Se for informativa, descreva:
import React from 'react';import { Image } from 'react-native';export function ProductImage({ uri, name }: { uri: string; name: string }) { return ( <Image source={{ uri }} accessibilityRole="image" accessibilityLabel={`Imagem do produto ${name}`} style={{ width: 96, height: 96 }} /> );}3) Campos de formulário
Associe rótulos e dicas. Em RN, muitas vezes o label fica no Text e o input precisa de accessibilityLabel coerente:
import React from 'react';import { Text, TextInput, View } from 'react-native';export function EmailField({ value, onChange }: { value: string; onChange: (t: string) => void }) { return ( <View> <Text>E-mail</Text> <TextInput value={value} onChangeText={onChange} autoCapitalize="none" keyboardType="email-address" accessibilityLabel="Campo de e-mail" /> </View> );}4) Componentes customizados (Switch/Checkbox)
Se você criar um componente que “parece” um switch/checkbox, exponha role e estado:
import React from 'react';import { Pressable, Text, View } from 'react-native';export function Toggle({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) { return ( <Pressable accessibilityRole="switch" accessibilityLabel={label} accessibilityState={{ checked: value }} onPress={() => onChange(!value)} hitSlop={10} > <View> <Text>{label}</Text> <Text>{value ? 'Ligado' : 'Desligado'}</Text> </View> </Pressable> );}Checklist rápido de a11y para revisar no código
| Item | Verificação | Exemplo |
|---|---|---|
| Elemento clicável tem label | Todo Pressable/Touchable tem accessibilityLabel quando o texto não é suficiente | Botão de ícone “Fechar” |
| Role correto | accessibilityRole condiz com a função | button, switch, header |
| Estado exposto | accessibilityState para checked/disabled/selected | { checked: true } |
| Área de toque | Use hitSlop e padding suficiente | hitSlop={12} |
| Contraste | Texto legível em temas claro/escuro | Tokens de cor e revisão visual |
Checklist de revisão para PRs (qualidade e manutenção)
Lint e formatação
npm run lintsem erros e sem warnings novos relevantes.npm run formatpassando (ouformat:writeaplicado).- Imports organizados (ordem consistente, sem duplicatas, sem não usados).
- Sem
console.logacidental e sem código morto.
Testes
- Novas regras de negócio têm testes unitários (casos normais + bordas).
- Componentes com interação têm testes de comportamento (toque, disabled, mensagens).
- Testes não dependem de detalhes internos; asserts são específicos e estáveis.
- Sem flakes: testes não dependem de tempo/ordem; mocks apenas quando necessário.
Acessibilidade
- Elementos interativos têm
accessibilityRoleeaccessibilityLabeladequados. - Estados (disabled/checked/selected) refletem o comportamento real.
- Área de toque adequada (padding/hitSlop), especialmente em ícones.
- Contraste revisado para texto e ícones em diferentes fundos/temas.
Manutenibilidade
- Nomes de funções/variáveis comunicam intenção; regras de negócio isoladas quando fizer sentido.
- Sem duplicação óbvia: extrações pequenas e úteis (sem abstrações prematuras).
- Scripts do projeto usados no PR (
lint,typecheck,test).