Qualidade e manutenção em React Native: lint, formatação, testes e acessibilidade

Capítulo 14

Tempo estimado de leitura: 9 minutos

+ Exercício

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-prettier

2) 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-jest

3) Crie (ou ajuste) o arquivo .eslintrc.js com um conjunto de regras pragmático:

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

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_modulesandroidiosdistbuildcoverage

Padronizando 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-native

2) 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, getByRole quando disponível) em vez de testID para 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 accessibilityLabel claro.
  • 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, checked devem 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

ItemVerificaçãoExemplo
Elemento clicável tem labelTodo Pressable/Touchable tem accessibilityLabel quando o texto não é suficienteBotão de ícone “Fechar”
Role corretoaccessibilityRole condiz com a funçãobutton, switch, header
Estado expostoaccessibilityState para checked/disabled/selected{ checked: true }
Área de toqueUse hitSlop e padding suficientehitSlop={12}
ContrasteTexto legível em temas claro/escuroTokens de cor e revisão visual

Checklist de revisão para PRs (qualidade e manutenção)

Lint e formatação

  • npm run lint sem erros e sem warnings novos relevantes.
  • npm run format passando (ou format:write aplicado).
  • Imports organizados (ordem consistente, sem duplicatas, sem não usados).
  • Sem console.log acidental 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 accessibilityRole e accessibilityLabel adequados.
  • 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).

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

Ao configurar ESLint e Prettier em um app React Native, qual abordagem ajuda a evitar conflitos e manter o código previsível no dia a dia?

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

Você errou! Tente novamente.

A prática recomendada é separar responsabilidades: o Prettier garante formatação determinística, enquanto o ESLint aplica regras de qualidade. Para não “brigar”, desativam-se no ESLint as regras que entram em conflito com o Prettier.

Próximo capitúlo

Build e distribuição em React Native: Android e iOS com ícones, splash e versões

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

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.