Estrutura de projeto em React Native e organização de código para apps reais

Capítulo 2

Tempo estimado de leitura: 12 minutos

+ Exercício

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 .service ou pasta por domínio (ex.: auth.service.ts).
  • Arquivos de estilos: styles.ts por tela/componente.
  • Constantes: UPPER_SNAKE_CASE quando forem valores fixos (ex.: DEFAULT_TIMEOUT), ou objetos nomeados (ex.: colors).
  • Pastas de tela: uma pasta por screen, com index.ts exportando 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.ts dentro de cada pasta de screen para exportar a tela.
  • Use components/index.ts para exportar apenas componentes realmente “públicos” e estáveis.
  • Evite um src/index.ts gigante exportando tudo.

Exemplo em src/screens/Login/index.ts:

export { LoginScreen as default } from './LoginScreen';

Exemplo em src/components/ui/index.ts:

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

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 em components).

Passo 2: definir tokens (cores e espaçamentos)

  • Crie colors.ts e spacing.ts.
  • Substitua valores repetidos nos estilos por tokens (ex.: padding: spacing.lg).

Passo 3: criar componentes UI reutilizáveis

  • Implemente Button, Input, Card com API pequena.
  • Garanta estados comuns: loading, disabled, error.

Passo 4: mover regra de negócio para hooks e services

  • Crie services/api/client.ts para 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 AppError e toUserMessage.
  • Crie logger centralizado e use em services (principalmente API).

Passo 6: configurar aliases de import

  • Adicione baseUrl e paths no tsconfig.json.
  • Atualize imports gradualmente para @components, @services, etc.

Checklist rápido de escalabilidade

Problema comumSinalAjuste recomendado
Duplicação de lógicaMesma regra em várias telasExtrair para hook ou utils
Componentes “fazem tudo”UI + fetch + navegação no mesmo arquivoService para integração, hook para orquestração
Imports longos e frágeis../../../../Aliases + barrels moderados
Estilos inconsistentesCores e espaçamentos diferentes sem motivoTokens em constants e tema
Tratamento de erro caóticoalert(err) em todo lugarAppError + toUserMessage + logger

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

Ao organizar um app React Native em camadas, qual combinação descreve corretamente a separação de responsabilidades para manter telas simples e reutilizar lógica?

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

Você errou! Tente novamente.

A abordagem recomendada separa responsabilidades: a screen foca em UI e composição, o hook centraliza a orquestração e estado local reutilizável, e o service encapsula integrações (API, storage, logger), reduzindo acoplamento e duplicação.

Próximo capitúlo

Componentes fundamentais do React Native e composição de UI

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

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.