Por que a estrutura do projeto importa em apps reais
Em projetos pequenos, qualquer organização “funciona”. Em apps reais, a estrutura define o quanto o time consegue evoluir o produto sem quebrar telas, duplicar lógica e perder tempo procurando código. Uma boa arquitetura prática em React Native costuma separar responsabilidades: UI (telas e componentes), regras de negócio (hooks e serviços), estado (store), navegação, e recursos (assets). O objetivo é reduzir acoplamento, aumentar reuso e manter previsível onde cada coisa deve ficar.
Estrutura base por camadas (sugestão prática)
Uma estrutura comum e escalável é organizar tudo dentro de src, separando por tipo de responsabilidade. Exemplo:
project-root/ src/ assets/ images/ fonts/ components/ ui/ feedback/ layout/ constants/ colors.ts spacing.ts typography.ts env.ts hooks/ useDebounce.ts useAsync.ts useAuth.ts navigation/ AppNavigator.tsx AuthNavigator.tsx linking.ts types.ts screens/ Login/ LoginScreen.tsx styles.ts index.ts Home/ HomeScreen.tsx styles.ts index.ts services/ api/ client.ts endpoints.ts auth/ auth.service.ts storage/ secureStorage.ts logger/ logger.ts store/ index.ts auth/ auth.slice.ts app/ app.slice.ts styles/ theme.ts global.ts utils/ formatters.ts validators.ts errors.ts App.tsx tsconfig.json- screens: telas (composição de UI + chamadas de hooks/serviços).
- components: componentes reutilizáveis e “burros” (UI), sem regra de negócio.
- hooks: lógica reutilizável (regras de negócio, orquestração, side effects).
- services: integração com APIs, storage, analytics, logger, etc.
- navigation: stacks/tabs e tipos de rotas.
- store: estado global (Redux/Zustand/Context), slices e selectors.
- constants: cores, espaçamentos, tipografia, flags de ambiente.
- styles: tema e estilos globais.
- utils: funções puras (formatadores, validadores, helpers).
Convenções de nomes (para reduzir ambiguidade)
- Componentes React:
PascalCase(ex.:PrimaryButton.tsx). - Hooks: prefixo
use(ex.:useAuth.ts). - Serviços: sufixo
.serviceou pasta por domínio (ex.:auth.service.ts). - Arquivos de estilos:
styles.tspor tela/componente. - Constantes:
UPPER_SNAKE_CASEquando forem valores fixos (ex.:DEFAULT_TIMEOUT), ou objetos nomeados (ex.:colors). - Pastas de tela: uma pasta por screen, com
index.tsexportando o componente principal.
Index files (barrel exports) sem exagero
Arquivos index.ts ajudam a simplificar imports, mas podem esconder dependências se usados em excesso. Uma estratégia equilibrada:
- Use
index.tsdentro de cada pasta de screen para exportar a tela. - Use
components/index.tspara exportar apenas componentes realmente “públicos” e estáveis. - Evite um
src/index.tsgigante exportando tudo.
Exemplo em src/screens/Login/index.ts:
export { LoginScreen as default } from './LoginScreen';Exemplo em src/components/ui/index.ts:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
export * from './Button/Button'; export * from './Input/Input'; export * from './Card/Card';Aliases de import (TypeScript) para imports limpos
Em apps reais, imports relativos longos viram ruído. Com TypeScript, configure paths no tsconfig.json:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@components/*": ["src/components/*"], "@screens/*": ["src/screens/*"], "@services/*": ["src/services/*"], "@constants/*": ["src/constants/*"], "@navigation/*": ["src/navigation/*"], "@store/*": ["src/store/*"], "@utils/*": ["src/utils/*"] } }}Depois, use:
import { Button } from '@components/ui'; import { colors } from '@constants/colors'; import { api } from '@services/api/client';Observação: dependendo do setup, você também precisará configurar o bundler (Metro) para reconhecer aliases. Uma abordagem comum é usar um plugin de resolução (ex.: via Babel) e manter os aliases consistentes entre TypeScript e bundler. O ponto principal é: defina uma única fonte de verdade e não crie aliases conflitantes.
Padrão de estilos: tema, tokens e estilos por arquivo
1) Tokens de design (cores e espaçamentos)
Centralize valores para evitar “números mágicos” espalhados.
src/constants/colors.ts:
export const colors = { brand: { primary: '#2F6FED', secondary: '#1B2B4A' }, text: { primary: '#0B1220', secondary: '#5B667A', inverse: '#FFFFFF' }, surface: { background: '#F6F8FC', card: '#FFFFFF' }, feedback: { danger: '#D92D20', success: '#12B76A' }} as const;src/constants/spacing.ts:
export const spacing = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, xxl: 32} as const;2) Tema (opcional, mas útil)
src/styles/theme.ts:
import { colors } from '@constants/colors'; import { spacing } from '@constants/spacing'; export const theme = { colors, spacing } as const; export type Theme = typeof theme;3) Estilos por componente/tela
Crie um styles.ts ao lado do componente/tela. Exemplo em src/screens/Login/styles.ts:
import { StyleSheet } from 'react-native'; import { colors } from '@constants/colors'; import { spacing } from '@constants/spacing'; export const styles = StyleSheet.create({ container: { flex: 1, padding: spacing.xl, backgroundColor: colors.surface.background }, title: { fontSize: 24, fontWeight: '700', color: colors.text.primary, marginBottom: spacing.lg }});Componentes reutilizáveis: Button, Input e Card
Componentes reutilizáveis devem ser previsíveis, com API pequena e foco em UI. Evite colocar neles chamadas de API, navegação ou regras de negócio.
Button
src/components/ui/Button/Button.tsx:
import React from 'react'; import { Pressable, Text, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native'; import { colors } from '@constants/colors'; import { spacing } from '@constants/spacing'; type ButtonVariant = 'primary' | 'secondary' | 'danger'; type Props = { title: string; onPress: () => void; disabled?: boolean; loading?: boolean; variant?: ButtonVariant; style?: ViewStyle; }; export function Button({ title, onPress, disabled = false, loading = false, variant = 'primary', style }: Props) { const isDisabled = disabled || loading; return ( <Pressable onPress={onPress} disabled={isDisabled} style={({ pressed }) => [ styles.base, styles[variant], isDisabled && styles.disabled, pressed && !isDisabled && styles.pressed, style ]} > {loading ? ( <ActivityIndicator color={colors.text.inverse} /> ) : ( <Text style={styles.title}>{title}</Text> )} </Pressable> ); } const styles = StyleSheet.create({ base: { height: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center', paddingHorizontal: spacing.lg }, title: { color: colors.text.inverse, fontWeight: '700' }, primary: { backgroundColor: colors.brand.primary }, secondary: { backgroundColor: colors.brand.secondary }, danger: { backgroundColor: colors.feedback.danger }, disabled: { opacity: 0.6 }, pressed: { opacity: 0.9 }});Input
src/components/ui/Input/Input.tsx:
import React from 'react'; import { View, Text, TextInput, StyleSheet, TextInputProps } from 'react-native'; import { colors } from '@constants/colors'; import { spacing } from '@constants/spacing'; type Props = TextInputProps & { label?: string; error?: string; }; export function Input({ label, error, style, ...rest }: Props) { return ( <View style={styles.container}> {!!label && <Text style={styles.label}>{label}</Text>} <TextInput placeholderTextColor={colors.text.secondary} style={[styles.input, !!error && styles.inputError, style]} {...rest} /> {!!error && <Text style={styles.error}>{error}</Text>} </View> ); } const styles = StyleSheet.create({ container: { marginBottom: spacing.md }, label: { marginBottom: spacing.xs, color: colors.text.secondary, fontWeight: '600' }, input: { height: 48, borderWidth: 1, borderColor: '#D0D5DD', borderRadius: 12, paddingHorizontal: spacing.md, backgroundColor: colors.surface.card, color: colors.text.primary }, inputError: { borderColor: colors.feedback.danger }, error: { marginTop: spacing.xs, color: colors.feedback.danger }});Card
src/components/ui/Card/Card.tsx:
import React, { ReactNode } from 'react'; import { View, StyleSheet, ViewStyle } from 'react-native'; import { colors } from '@constants/colors'; import { spacing } from '@constants/spacing'; type Props = { children: ReactNode; style?: ViewStyle; }; export function Card({ children, style }: Props) { return <View style={[styles.card, style]}>{children}</View>; } const styles = StyleSheet.create({ card: { backgroundColor: colors.surface.card, borderRadius: 16, padding: spacing.lg, shadowColor: '#000', shadowOpacity: 0.08, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 2 }});Isolando regras de negócio: hooks e services
Uma regra prática: screen compõe, hook orquestra, service executa. Assim, a tela fica focada em UI e fluxo, e a lógica pode ser testada e reutilizada.
Service de API (client) e endpoints
src/services/api/client.ts (exemplo simples com fetch):
import { AppError } from '@utils/errors'; import { logger } from '@services/logger/logger'; const BASE_URL = 'https://api.example.com'; type RequestOptions = { method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; body?: unknown; headers?: Record<string, string>; timeoutMs?: number; }; async function request<T>(path: string, options: RequestOptions = {}): Promise<T> { const { method = 'GET', body, headers, timeoutMs = 15000 } = options; const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { const res = await fetch(`${BASE_URL}${path}`, { method, headers: { 'Content-Type': 'application/json', ...(headers ?? {}) }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal }); const text = await res.text(); const data = text ? JSON.parse(text) : null; if (!res.ok) { logger.warn('API_ERROR', { path, status: res.status, data }); throw new AppError('REQUEST_FAILED', 'Não foi possível completar a requisição.', { status: res.status, data }); } return data as T; } catch (err: any) { logger.error('API_EXCEPTION', { path, message: err?.message }); if (err?.name === 'AbortError') { throw new AppError('TIMEOUT', 'Tempo de requisição excedido.'); } throw err; } finally { clearTimeout(id); } } export const api = { get: <T>(path: string, headers?: Record<string, string>) => request<T>(path, { method: 'GET', headers }), post: <T>(path: string, body?: unknown, headers?: Record<string, string>) => request<T>(path, { method: 'POST', body, headers }) };Service de autenticação
src/services/auth/auth.service.ts:
import { api } from '@services/api/client'; export type LoginDTO = { email: string; password: string }; export type LoginResponse = { token: string; user: { id: string; name: string; email: string } }; export async function login(dto: LoginDTO): Promise<LoginResponse> { return api.post<LoginResponse>('/auth/login', dto); }Hook de autenticação (orquestração + estado local)
src/hooks/useAuth.ts:
import { useCallback, useState } from 'react'; import * as authService from '@services/auth/auth.service'; import { toUserMessage } from '@utils/errors'; export function useAuth() { const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const signIn = useCallback(async (email: string, password: string) => { setLoading(true); setError(null); try { const result = await authService.login({ email, password }); return result; } catch (e) { setError(toUserMessage(e)); throw e; } finally { setLoading(false); } }, []); return { loading, error, signIn }; }Screen usando componentes + hook
src/screens/Login/LoginScreen.tsx:
import React, { useState } from 'react'; import { View, Text } from 'react-native'; import { Button } from '@components/ui/Button/Button'; import { Input } from '@components/ui/Input/Input'; import { Card } from '@components/ui/Card/Card'; import { useAuth } from '@hooks/useAuth'; import { styles } from './styles'; export function LoginScreen() { const { loading, error, signIn } = useAuth(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); async function handleLogin() { await signIn(email.trim(), password); } return ( <View style={styles.container}> <Text style={styles.title}>Entrar</Text> <Card> <Input label="E-mail" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" /> <Input label="Senha" value={password} onChangeText={setPassword} secureTextEntry /> <Button title="Acessar" onPress={handleLogin} loading={loading} /> {!!error && <Text style={{ marginTop: 12, color: 'red' }}>{error}</Text>} </Card> </View> ); }Store: quando usar e como manter organizado
Use estado global quando múltiplas telas precisam do mesmo dado (usuário logado, feature flags, carrinho, preferências). Para estado local de tela (inputs, loading pontual), prefira useState e hooks.
Organização sugerida:
store/auth/auth.slice.ts: estado e ações de auth.store/index.ts: configuração e exports do store.store/*/selectors.ts: seletores para evitar acessar estrutura interna diretamente.
Mesmo que você use Redux, Zustand ou Context, o princípio é o mesmo: separar por domínio e expor uma API estável (actions/selectors).
Navegação: separar tipos e rotas
Evite declarar rotas dentro das screens. Centralize em src/navigation:
types.ts: tipos de parâmetros de rotas.AppNavigator.tsx: navegação principal.AuthNavigator.tsx: fluxo de autenticação.
Exemplo de tipos em src/navigation/types.ts:
export type AuthStackParamList = { Login: undefined; ForgotPassword: undefined; }; export type AppStackParamList = { Home: undefined; Profile: { userId: string }; };Estratégia de erros e logs (desenvolvimento)
Apps reais precisam de um padrão para: (1) classificar erros, (2) exibir mensagens amigáveis, (3) registrar detalhes técnicos para depuração.
1) Modelo de erro da aplicação
src/utils/errors.ts:
export class AppError extends Error { code: string; details?: unknown; constructor(code: string, message: string, details?: unknown) { super(message); this.code = code; this.details = details; } } export function toUserMessage(err: unknown): string { if (err instanceof AppError) return err.message; return 'Ocorreu um erro inesperado. Tente novamente.'; }2) Logger centralizado
src/services/logger/logger.ts:
type LogLevel = 'debug' | 'info' | 'warn' | 'error'; function log(level: LogLevel, tag: string, payload?: unknown) { const base = `[${level.toUpperCase()}] ${tag}`; if (payload) { console.log(base, payload); } else { console.log(base); } } export const logger = { debug: (tag: string, payload?: unknown) => log('debug', tag, payload), info: (tag: string, payload?: unknown) => log('info', tag, payload), warn: (tag: string, payload?: unknown) => log('warn', tag, payload), error: (tag: string, payload?: unknown) => log('error', tag, payload) };Boas práticas:
- Logue com tag (ex.:
API_ERROR,AUTH_LOGIN) para filtrar facilmente. - Não exiba detalhes técnicos para o usuário; converta para mensagem amigável via
toUserMessage. - Evite logar dados sensíveis (senha, token completo). Se precisar, mascare.
Passo a passo prático: aplicando a arquitetura em um projeto
Passo 1: criar as pastas base em src
- Crie:
screens,components,hooks,services,navigation,store,constants,styles,utils,assets. - Mova código existente para a camada correta (telas em
screens, componentes emcomponents).
Passo 2: definir tokens (cores e espaçamentos)
- Crie
colors.tsespacing.ts. - Substitua valores repetidos nos estilos por tokens (ex.:
padding: spacing.lg).
Passo 3: criar componentes UI reutilizáveis
- Implemente
Button,Input,Cardcom API pequena. - Garanta estados comuns:
loading,disabled,error.
Passo 4: mover regra de negócio para hooks e services
- Crie
services/api/client.tspara padronizar requisições. - Crie serviços por domínio (ex.:
services/auth). - Crie hooks que chamam serviços e expõem estado (ex.:
useAuth).
Passo 5: padronizar erros e logs
- Crie
AppErroretoUserMessage. - Crie
loggercentralizado e use em services (principalmente API).
Passo 6: configurar aliases de import
- Adicione
baseUrlepathsnotsconfig.json. - Atualize imports gradualmente para
@components,@services, etc.
Checklist rápido de escalabilidade
| Problema comum | Sinal | Ajuste recomendado |
|---|---|---|
| Duplicação de lógica | Mesma regra em várias telas | Extrair para hook ou utils |
| Componentes “fazem tudo” | UI + fetch + navegação no mesmo arquivo | Service para integração, hook para orquestração |
| Imports longos e frágeis | ../../../../ | Aliases + barrels moderados |
| Estilos inconsistentes | Cores e espaçamentos diferentes sem motivo | Tokens em constants e tema |
| Tratamento de erro caótico | alert(err) em todo lugar | AppError + toUserMessage + logger |