O que Expo faz?
Padrões práticos para construir aplicativos React Native de produção com Expo. Abrange navegação, estado, busca de dados, listas, estilização e APIs nativas. Funciona em conjunto com o conjunto de regras rules/react-native/: as regras dizem o que aplicar, este skill mostra como.
As bibliotecas citadas abaixo (NativeWind, Zustand/Jotai, TanStack Query) são opções comuns e bem estabelecidas mostradas como ilustração — os padrões importam mais do que o pacote específico, e qualquer equivalente funciona. Zod é usado para validação para manter consistência com as regras existentes de typescript/ da ECC.
Estes padrões assumem o fluxo de trabalho gerenciado do Expo (Expo Router, EAS, módulos expo-*) na Nova Arquitetura (o padrão em SDKs recentes do Expo, obrigatório a partir do SDK 55+). Eles NÃO assumem o DOM do navegador — React Native não tem <div>, nem barra de URL, nem padrões de busca de dados da web.
Quando Ativar
Use este skill quando:
- Construindo ou editando telas, componentes ou navegação React Native / Expo
- Configurando roteamento com Expo Router (diretório
app/baseado em arquivos) - Decidindo onde o estado pertence (cache do servidor vs store do cliente vs parâmetros de rota vs formulário)
- Conectando busca de dados com TanStack Query e validando respostas com Zod
- Renderizando listas longas ou pesadas
- Escolhendo ou aplicando uma abordagem de estilização (NativeWind ou StyleSheet)
- Acessando APIs de dispositivos nativos (câmera, localização, notificações) ou armazenamento seguro
- Revisando código RN para problemas específicos de mobile
NÃO use os padrões web/React-DOM aqui — URL-como-estado, <div>, e SWR-para-navegador não se aplicam ao React Native.
Conceitos Principais
Estrutura do projeto (Expo Router)
Roteamento baseado em arquivos sob app/. Mantenha os arquivos de rota enxutos: eles leem e validam parâmetros, depois delegam a um componente de tela que vive em components/ ou features/.
app/
_layout.tsx # pilha raiz
(tabs)/
_layout.tsx # navegador de abas
index.tsx # Início
user/[id].tsx # rota dinâmica
components/
features/
user/UserProfile.tsx
Navegação: valide parâmetros de rota
Links profundos e rotas dinâmicas entregam strings não confiáveis. Valide-as com Zod antes de usar.
// app/user/[id].tsx
import { useLocalSearchParams, router } from 'expo-router'
import { z } from 'zod'
import { UserProfile } from '@/features/user/UserProfile'
const Params = z.object({ id: z.string().uuid() })
export default function UserRoute() {
const parsed = Params.safeParse(useLocalSearchParams())
if (!parsed.success) {
router.replace('/not-found')
return null
}
return <UserProfile userId={parsed.data.id} />
}
Estado: mantenha as preocupações separadas
Não duplique dados do servidor em uma store do cliente. Cada preocupação tem seu próprio lar.
| Preocupação | Escolhas comuns |
|---|---|
| Estado do servidor (dados remotos) | uma biblioteca de cache de servidor (TanStack Query, SWR) |
| Estado do cliente/UI | uma store leve (Zustand, Jotai) ou Context |
| Estado de rota/navegação | Parâmetros do Expo Router |
| Estado de formulário | uma biblioteca de formulários (ex.: React Hook Form) + validação de esquema |
| Segredos / tokens | expo-secure-store |
| Persistência não secreta | AsyncStorage / MMKV |
Prefira useState local até que o estado realmente precise ser compartilhado.
Busca de dados: uma biblioteca de cache + Zod
Use uma biblioteca de cache de servidor (TanStack Query, SWR) em vez de fetch-em-useEffect. Valide na fronteira e infira tipos a partir do esquema. Trate os estados de carregamento, erro e vazio explicitamente. (O exemplo usa TanStack Query.)
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { z } from 'zod'
const User = z.object({ id: z.string(), email: z.string().email() })
type User = z.infer<typeof User>
export function useUser(id: string) {
return useQuery({
queryKey: ['user', id],
queryFn: async (): Promise<User> => User.parse(await api.getUser(id)),
})
}
export function useUpdateEmail(id: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (email: string) => api.updateEmail(id, email),
onSuccess: () => qc.invalidateQueries({ queryKey: ['user', id] }),
})
}
Listas: virtualize, nunca mapeie um array grande em um ScrollView
import { FlatList } from 'react-native'
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem} // memoizado
initialNumToRender={10}
windowSize={5}
/>
Use FlashList (Shopify) para listas grandes ou heterogêneas.
Estilização: escolha um sistema
StyleSheet.create() é a opção nativa do framework; bibliotecas de classes utilitárias (ex.: NativeWind) são uma alternativa comum. Escolha uma e mantenha a consistência. Nunca construa objetos de estilo inline no JSX em caminhos críticos.
// NativeWind
<View className="p-4 rounded-2xl bg-white">
<Text className="text-base font-semibold">Hello</Text>
</View>
// StyleSheet
const styles = StyleSheet.create({ card: { padding: 16, borderRadius: 16, backgroundColor: '#fff' } })
<View style={styles.card}>...</View>
APIs nativas: envolva em hooks, limpe os efeitos
Mantenha chamadas e assinaturas do Expo SDK dentro de hooks use*, não no JSX. Sempre faça a limpeza.
import { useEffect, useState } from 'react'
import * as Location from 'expo-location'
type LocationState =
| { status: 'loading' }
| { status: 'denied' }
| { status: 'granted'; coords: Location.LocationObjectCoords }
export function useCurrentLocation() {
// Rastreie o status, não apenas as coordenadas — para que a UI possa distinguir "ainda carregando"
// de "permissão negada" e mostrar uma mensagem acionável.
const [state, setState] = useState<LocationState>({ status: 'loading' })
useEffect(() => {
let active = true
;(async () => {
const { status } = await Location.requestForegroundPermissionsAsync()
if (status !== 'granted') {
if (active) setState({ status: 'denied' })
return
}
const pos = await Location.getCurrentPositionAsync({})
if (active) setState({ status: 'granted', coords: pos.coords })
})()
return () => { active = false } // ignore resultado obsoleto após desmontagem
}, [])
return state
}
Armazenamento seguro para tokens
import * as SecureStore from 'expo-secure-store'
await SecureStore.setItemAsync('auth_token', token) // Keychain / Keystore
const token = await SecureStore.getItemAsync('auth_token')
Exemplos de Código
Uma tela completa: rota → query → lista → estados
// app/(tabs)/orders.tsx
import { memo, useCallback } from 'react'
import { FlatList, Text, View } from 'react-native'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
const OrderSchema = z.object({ id: z.string(), total: z.number(), status: z.string() })
const OrdersSchema = z.array(OrderSchema)
type Order = z.infer<typeof OrderSchema>
function useOrders() {
return useQuery({
queryKey: ['orders'],
queryFn: async () => OrdersSchema.parse(await api.listOrders()),
})
}
// Memoizado para que sua referência seja estável entre renderizações (veja a orientação sobre listas).
const OrderRow = memo(function OrderRow({ item }: { item: Order }) {
return (
<View className="px-4 py-3 border-b border-neutral-200">
<Text className="font-medium">#{item.id}</Text>
<Text className="text-neutral-500">{item.status} · ${item.total}</Text>
</View>
)
})
export default function OrdersScreen() {
const { data, isLoading, isError, refetch, isRefetching } = useOrders()
const renderItem = useCallback(({ item }: { item: Order }) => <OrderRow item={item} />, [])
if (isLoading) return <Centered><Text>Carregando…</Text></Centered>
if (isError) return <Centered><Text accessibilityRole="alert">Não foi possível carregar os pedidos.</Text></Centered>
if (!data?.length) return <Centered><Text>Nenhum pedido ainda.</Text></Centered>
return (
<FlatList
data={data}
keyExtractor={(o) => o.id}
onRefresh={refetch}
refreshing={isRefetching}
renderItem={renderItem}
/>
)
}
Um formulário: React Hook Form + resolvedor Zod
import { useForm, Controller } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { TextInput, Button, Text } from 'react-native'
const Schema = z.object({ email: z.string().email('Email inválido') })
type FormValues = z.infer<typeof Schema>
export function EmailForm({ onSubmit }: { onSubmit: (v: FormValues) => void }) {
const { control, handleSubmit, formState: { errors } } = useForm<FormValues>({
resolver: zodResolver(Schema),
defaultValues: { email: '' },
})
return (
<>
<Controller
control={control}
name="email"
render={({ field: { value, onChange, onBlur } }) => (
<TextInput
value={value}
onChangeText={onChange}
onBlur={onBlur}
autoCapitalize="none"
keyboardType="email-address"
accessibilityLabel="Endereço de email"
/>
)}
/>
{errors.email && <Text accessibilityRole="alert">{errors.email.message}</Text>}
<Button title="Salvar" onPress={handleSubmit(onSubmit)} />
</>
)
}
Antipadrões
// ERRADO: array grande mapeado dentro de um ScrollView (sem virtualização, instável, alta memória)
<ScrollView>{items.map((i) => <Row key={i.id} item={i} />)}</ScrollView>
// CORRETO: FlatList / FlashList
// ERRADO: dados do servidor copiados para uma store do cliente (duas fontes de verdade, dados obsoletos)
const useStore = create((set) => ({ users: [], setUsers: (u) => set({ users: u }) }))
useEffect(() => { getUsers().then(setUsers) }, [])
// CORRETO: useQuery possui o estado do servidor; derive o que precisar
// ERRADO: tokens no AsyncStorage (não criptografado)
await AsyncStorage.setItem('auth_token', token)
// CORRETO: expo-secure-store
// ERRADO: confiar em parâmetros de deep link
const { id } = useLocalSearchParams(); fetchUser(id)
// CORRETO: validar com Zod antes de usar
// ERRADO: objeto de estilo inline recriado a cada renderização em um caminho crítico
<View style={{ padding: 16, backgroundColor: '#fff' }} />
// CORRETO: StyleSheet.create no escopo do módulo, ou className do NativeWind
// ERRADO: segredo real enviado no bundle
const STRIPE_SECRET = 'sk_live_...'
// CORRETO: mantenha chamadas privilegiadas no servidor; envie apenas chaves públicas protegidas por regras de backend
Melhores Práticas
- Mantenha arquivos de rota enxutos; coloque a lógica em componentes de tela e hooks
use*. - Valide toda entrada externa (respostas de API, parâmetros de rota, payloads de push) com Zod.
- Deixe o TanStack Query ser o dono do estado do servidor; mantenha as stores do cliente pequenas.
- Sempre renderize estados de carregamento, erro e vazio — nunca apenas um spinner sem fallback.
- Virtualize listas; memoize
renderItem; forneça umkeyExtractorestável. - Use
react-native-reanimatedpara animações (thread da UI); evite trabalho pesado na thread JS. - Armazene tokens em
expo-secure-store; nunca confie no cliente para autorização. - Respeite áreas seguras, Dynamic Type e papéis/rótulos de acessibilidade desde o início.
- Confirme a compatibilidade com a Nova Arquitetura para cada dependência nativa antes do lançamento.
Skills Relacionados
frontend-patterns— Padrões React/Next.js (web); útil para conceitos React compartilhados, mas específicos do DOM.coding-standards— Expressões idiomáticas TypeScript/JavaScript que se aplicam ao código RN.tdd-workflow,e2e-testing— Processo de teste (use Jest + React Native Testing Library, Maestro/Detox para RN).security-review— Lista de verificação de segurança geral que complementa a orientação sobre bundle/segredos RN acima.