Artigo original: https://www.freecodecamp.org/news/how-to-structure-your-project-and-manage-static-resources-in-react-native-6f4cfc947d92/

Escrito por: Khoa Pham

React e React Native são apenas frameworks, e não ditam como devemos estruturar nossos projetos. Tudo depende do seu gosto pessoal e do projeto em que você está trabalhando.

Neste post, vamos abordar como estruturar um projeto e como gerenciar ativos locais. Isso, claro, não está escrito em pedra, e você é livre para aplicar apenas as peças que lhe convierem. Espero que você aprenda alguma coisa.

Para um projeto inicializado com react-native-init, obtemos apenas a estrutura básica (texto em inglês).

Há a pasta ios para projetos do Xcode, a pasta android para projetos para o Android e arquivos index.js e App.js para o ponto de partida do React Native.

ios/
android/
index.js
App.js

Por já ter trabalhado com arquivos nativos no Windows Phone, no iOS e no Android, penso que estruturar um projeto se resume a separar arquivos por tipo ou por recurso.

Tipo x recurso

Separar por tipo significa organizarmos os arquivos por seu tipo. Se o tipo for um componente, haverá arquivos de contêiner e arquivos de apresentação. Se for do Redux, haverá arquivos de ação, reducers e arquivos de armazenamento. Se for uma view, haverá arquivos JavaScript, HTML e CSS.

Agrupar por tipo

redux
  actions
  store
  reducers
components
  container
  presentational
view
  javascript
  html
  css

Desse modo, podemos ver o tipo de cada arquivo e executar facilmente um script para determinado tipo de arquivo. Isso vale para todos os projetos, mas não responde à pergunta "Qual é o objetivo deste projeto?" É uma aplicação de notícias? É uma aplicação de fidelização? É um acompanhamento nutricional?

Organizar arquivos por tipo é útil para máquinas, mas não para humanos. Muitas vezes, trabalhamos em recursos. Encontrar arquivos para corrigir em diversos diretórios é muito cansativo. Também é um incômodo quando planejamos fazer a estrutura de nosso projeto, já que os arquivos estão espalhados por muitos lugares.

Agrupar por recurso

Uma solução mais razoável é organizar arquivos por recurso. Os arquivos relacionados a um recurso devem ser colocados juntos. Os arquivos de teste devem ficar próximos aos arquivos de origem. Confira este artigo (texto em inglês) para saber mais.

Um recurso pode estar relacionado ao login, inscrição, integração ou ao perfil de um usuário. Um recurso pode conter sub-recursos, desde que pertençam ao mesmo fluxo. Se quiséssemos mover o sub-recurso, seria fácil, pois todos os arquivos relacionados já estariam agrupados.

Minha estrutura típica de projeto baseada em recursos tem a seguinte aparência:

index.js
App.js
ios/
android/
src
  screens
    login
      LoginScreen.js
      LoginNavigator.js
    onboarding
      OnboardingNavigator    
      welcome 
        WelcomeScreen.js
      term
        TermScreen.js
      notification
        NotificationScreen.js
    main
      MainNavigator.js
      news
        NewsScreen.js
      profile
        ProfileScreen.js
      search
        SearchScreen.js
  library
    package.json
    components
      ImageButton.js
      RoundImage.js
    utils
      moveToBottom.js
      safeArea.js
    networking
      API.js
      Auth.js
  res
    package.json
    strings.js
    colors.js
    palette.js
    fonts.js
    images.js
    images
      logo@2x.png
      logo@3x.png
      button@2x.png
      button@3x.png
scripts
  images.js
  clear.js

Além dos arquivos App.js e index.js e das pastas ios1 e android tradicionais, coloquei todos os arquivos de origem na pasta src. Dentro de src, tenho res, para recursos (do inglês, resources), library, para arquivos comuns usados em mais de um recurso, e screens, para a tela de conteúdo.

O menor número de dependências possível

Como o React Native é fortemente dependente de uma infinidade de dependências, tento tomar muito cuidado ao adicionar mais dependências. No meu projeto, uso apenas react-navigation para navegação. Não sou um fã do redux. Acho que ele adiciona uma complexidade desnecessária. Só adicione uma dependência quando você realmente precisar dela. Caso contrário, você está apenas se preparando para ter mais problemas.

O que eu gosto no React são os componentes. Um componente é onde definimos visão, estilo e comportamento. O React tem estilo em linha — é como usar JavaScript para definir scripts, HTML e CSS. Isso se encaixa na abordagem de recursos que estamos buscando. É por isso que eu não uso o styled-components. Como os estilos são apenas objetos JavaScript, podemos simplesmente compartilhar estilos de comentário em library.

src

Gosto muito do Android. Por isso, coloco os nomes src e res para corresponder às convenções de pasta do sistema.

O react-native init configura o babel para nós. Porém, em um projeto em JavaScript típico, é bom organizar os arquivos na pasta src. Na minha aplicação IconGenerator, que usa o electron.js, coloquei os arquivos de origem dentro da pasta src. Isso não só ajuda em termos de organização, mas também ajuda o babel a transpilar toda a pasta de uma só vez. Basta um comando e eu tenho os arquivos de src transpilados para dist em um piscar de olhos.

babel ./src --out-dir ./dist --copy-files

Screen

O React é baseado em componentes. Sim. Existem componentes de contêiner e de apresentação (texto em inglês), mas podemos compor componentes para criar componentes mais complexos. Eles, geralmente, terminam aparecendo em tela cheia. Chama-se Page no Windows Phone, ViewController no iOS e Activity no Android. O guia do React Native menciona a tela (em inglês, screen) com muita frequência como algo que cobre todo o espaço:

As aplicações para dispositivos móveis raramente são compostas de uma única tela. O gerenciamento da apresentação e da transição entre várias telas normalmente é manipulado pelo que é conhecido como navegador.

Usar ou não usar o index.js?

Cada tela é considerada o ponto de entrada para um recurso. Você pode renomear LoginScreen.js como index.js para aproveitar o recurso de módulos do Node:

find-me  node_modules  index.js  require('find-me')  index.js

Assim, em vez de import LoginScreen from './screens/LoginScreen', podemos simplesmente usar import LoginScreen from './screens'.

Usar o index.js resulta em encapsulamento e fornece uma interface pública para o recurso. Isso tudo se baseia em gosto pessoal. Eu mesmo prefiro nomear explicitamente um arquivo – daí o nome LoginScreen.js.

O react-navigation parece ser a escolha mais popular para lidar com a navegação em uma aplicação do React Native. Para um recurso como a integração (do inglês, onboarding), provavelmente, existem muitas telas gerenciadas por uma navegação pela pilha. Assim, temos o OnboardingNavigator.

Pense no Navigator como algo que agrupe subtelas ou recursos. Como estamos agrupando por recurso, é natural colocarmos o navegador dentro da pasta de recursos. A aparência, basicamente, é esta:

import { createStackNavigator } from 'react-navigation'
import Welcome from './Welcome'
import Term from './Term'

const routeConfig = {
  Welcome: {
    screen: Welcome
  },
  Term: {
    screen: Term
  }
}

const navigatorConfig = {
  navigationOptions: {
    header: null
  }
}

export default OnboardingNavigator = createStackNavigator(routeConfig, navigatorConfig)

library

Esta é a parte mais polêmica da estruturação de um projeto. Se você não gosta do nome library, pode chamá-la de utilities, common, citadel, ou o que quer que seja...

Essa pasta não é destinada a arquivos sem outro lugar para ficar, mas é onde colocamos utilitários e componentes comuns, que são usados por muitos recursos. Coisas como componentes atômicos, wrappers, função de correções rápidas, coisas relacionadas à rede e informações de login são muito usadas, sendo difícil movê-las para uma pasta de recursos específica. Às vezes, só precisamos ser práticos e fazer o trabalho.

No React Native, muitas vezes, precisamos implementar um botão com uma imagem de fundo em muitas telas. Aqui está um botão simples, colocado em library/components/ImageButton.js. A pasta components é para componentes reutilizáveis, às vezes chamados de componentes atômicos. De acordo com as convenções de nomenclatura do React, a primeira letra deve ser maiúscula.

import React from 'react'
import { TouchableOpacity, View, Image, Text, StyleSheet } from 'react-native'
import images from 'res/images'
import colors from 'res/colors'

export default class ImageButton extends React.Component {
  render() {
    return (
      <TouchableOpacity style={styles.touchable} onPress={this.props.onPress}>
        <View style={styles.view}>
          <Text style={styles.text}>{this.props.title}</Text>
        </View>
        <Image
          source={images.button}
          style={styles.image} />
      </TouchableOpacity>
    )
  }
}

const styles = StyleSheet.create({
  view: {
    position: 'absolute',
    backgroundColor: 'transparent'
  },
  image: {
  
},
  touchable: {
    alignItems: 'center',
    justifyContent: 'center'
  },
  text: {
    color: colors.button,
    fontSize: 18,
    textAlign: 'center'
  }
})

Se quisermos colocar o botão na parte inferior, usamos uma função utilitária para evitar a duplicação de código. Aqui temos a library/utils/moveToBottom.js:

import React from 'react'
import { View, StyleSheet } from 'react-native'

function moveToBottom(component) {
  return (
    <View style={styles.container}>
      {component}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-end',
    marginBottom: 36
  }
})

export default moveToBottom

Usando o package.json para evitar caminhos relativos

Em algum ponto de src/screens/onboarding/term/Term.js, podemos importar usando caminhos relativos:

import moveToBottom from '../../../../library/utils/move'
import ImageButton from '../../../../library/components/ImageButton'

Aqui, temos um problema bem visível. A chance de cometermos erros é muito grande, pois precisamos calcular de quantos .. precisamos. Se movermos o recurso para outro lugar, todos esses caminhos precisarão ser recalculados.

Como library é para ser usado em muitos lugares, é bom referenciá-la como um caminho absoluto. Em JavaScript, geralmente, existem milhares de bibliotecas para um único problema. Uma rápida pesquisa no Google revela centenas de bibliotecas para resolver esse problema. Porém, não precisamos de outra dependência, pois isso é extremamente fácil de corrigir.

A solução é transformar library em um módulo module para que o node consiga encontrá-la. Adicionar o package.json a qualquer pasta a torna um módulo no Node. Adicionamos o package.json dentro da pasta library com este conteúdo simples:

{
  "name": "library",
  "version": "0.0.1"
}

Agora, em Term.js, podemos importar as coisas de library facilmente, pois, agora, a pasta se tornou um módulo:

import React from 'react'
import { View, StyleSheet, Image, Text, Button } from 'react-native'
import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'
import ImageButton from 'library/components/ImageButton'
import moveToBottom from 'library/utils/moveToBottom'

export default class Term extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.heading}>{strings.onboarding.term.heading.toUpperCase()}</Text>
        {
          moveToBottom(
            <ImageButton style={styles.button} title={strings.onboarding.term.button.toUpperCase()} />
          )
        }
      </View>
    )
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center'
  },
  heading: {...palette.heading, ...{
    marginTop: 72
  }}
})

res

Você pode estar se perguntando o que são res/colors, res/strings, res/images e res/fonts nos exemplos acima. Bem, para projetos de front-end, geralmente temos componentes e os estilizamos usando fontes, strings com localização (traduzidas), cores, imagens e estilos propriamente ditos. O JavaScript é uma linguagem muito dinâmica, sendo fácil usar tipos em formato string em qualquer lugar. Podemos ter várias definições do tipo color: #00B75D; em vários arquivos, ou fontFamily: Fira; em vários componentes do tipo Text. Isso também leva a erros e é difícil de refatorar.

Vamos encapsular o uso dos recursos na pasta res com objetos mais seguros. Veja a aparência disso nos exemplos a seguir:

res/colors

const colors = {
  title: '#00B75D',
  text: '#0C222B',
  button: '#036675'
}

export default colors

res/strings

const strings = {
  onboarding: {
    welcome: {
      heading: 'Welcome',
      text1: "What you don't know is what you haven't learn",
      text2: 'Visit my GitHub at https://github.com/onmyway133',
      button: 'Log in'
    },
    term: {
      heading: 'Terms and conditions',
      button: 'Read'
    }
  }
}

export default strings

res/fonts

const fonts = {
  title: 'Arial',
  text: 'SanFrancisco',
  code: 'Fira'
}

export default fonts

res/images

const images = {
  button: require('./images/button.png'),
  logo: require('./images/logo.png'),
  placeholder: require('./images/placeholder.png')
}

export default images

Assim como em library, os arquivos de res podem ser acessados de qualquer lugar. Assim, vamos tornar a pasta res um módulo. Adicione o package.json à pasta res:

{
  "name": "res",
  "version": "0.0.1"
}

Desse modo, podemos acessar a pasta de recursos como se fosse um módulo normal:

import strings from 'res/strings'
import palette from 'res/palette'
import images from 'res/images'

Agrupar cores, imagens e fontes com uma paleta

O design da aplicação deve ser consistente. Certos elementos devem ter a mesma aparência para não confundir o usuário. Por exemplo, os títulos do componente Text devem usar uma cor, fonte e tamanho de fonte iguais. O componente Image deve usar a mesma imagem placeholder. No React Native, já usamos o nome styles em const styles = StyleSheet.create({}). Então, vamos usar o nome palette (em português, paleta).

Abaixo, vemos minha paleta simplificada. Ela define estilos comuns para os títulos e para Text:

res/palette

import colors from './colors'

const palette = {
  heading: {
    color: colors.title,
    fontSize: 20,
    textAlign: 'center'
  },
  text: {
    color: colors.text,
    fontSize: 17,
    textAlign: 'center'
  }
}

export default palette

Também podemos usá-los em nossa tela:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center'
  },
  heading: {...palette.heading, ...{
    marginTop: 72
  }}
})

Aqui, usamos o operador spread em um objeto para unir palette.heading e nosso objeto de estilo personalizado. Isso quer dizer que usamos estilos de palette.heading, mas também especificamos outras propriedades.

Se fôssemos remodelar a aplicação para diversas marcas, poderíamos ter diversas paletas. Esse é um padrão muito útil.

Geração de imagens

Podemos ver que, em /src/res/images.js, temos propriedades para cada imagem na pasta src/res/images:

const images = {
  button: require('./images/button.png'),
  logo: require('./images/logo.png'),
  placeholder: require('./images/placeholder.png')
}

export default images

É um tédio fazer isso manualmente. Teríamos que atualizar se houvesse mudanças na convenção de nomenclatura de imagem. Em vez disso, podemos adicionar um script para gerar images.js com base nas imagens que temos. Adicione um arquivo /scripts/images.js na raiz do projeto:

const fs = require('fs')

const imageFileNames = () => {
  const array = fs
    .readdirSync('src/res/images')
    .filter((file) => {
      return file.endsWith('.png')
    })
    .map((file) => {
      return file.replace('@2x.png', '').replace('@3x.png', '')
    })
    
return Array.from(new Set(array))
}

const generate = () => {
  let properties = imageFileNames()
    .map((name) => {
      return `${name}: require('./images/${name}.png')`
    })
    .join(',\n  ')
    
const string = `const images = {
  ${properties}
}

export default images
`

fs.writeFileSync('src/res/images.js', string, 'utf8')
}

generate()

No Node, temos um recurso interessante que é o acesso ao módulo fs, muito útil no processamento de arquivos. Aqui, simplesmente percorremos as imagens e atualizamos /src/res/images.js conforme o caso.

Sempre que adicionarmos ou alterarmos imagens, podemos executar:

node scripts/images.js

Também podemos declarar o script dentro de nosso package.json principal:

"scripts": {
  "start": "node node_modules/react-native/local-cli/cli.js start",
  "test": "jest",
  "lint": "eslint *.js **/*.js",
  "images": "node scripts/images.js"
}

Agora, basta executar npm run images e obtemos o arquivo de recurso images.js atualizado.

Fontes personalizadas

O React Native tem algumas fontes personalizadas (texto em inglês) que podem ser boas o suficiente para seu projeto. Você também pode usar fontes personalizadas.

Uma coisa que se deve perceber é que o Android usa o nome do arquivo de fonte, mas o iOS usa o nome completo. Você pode ver o nome completo na aplicação Font Book ou inspecionando na aplicação em execução

for (NSString* family in [UIFont familyNames]) {
  NSLog(@"%@", family);
  
for (NSString* name in [UIFont fontNamesForFamilyName: family]) {
    NSLog(@"Family name:  %@", name);
  }
}

Para fontes personalizadas a serem registradas no iOS, precisamos declarar UIAppFonts em Info.plist usando o nome do arquivo das fontes. Para o Android, as fontes precisam ser colocadas em app/src/main/assets/fonts.

É uma boa prática nomear o arquivo de fonte da mesma forma que o nome completo. Diz-se que o React Native carrega dinamicamente fontes personalizadas, mas, no caso de você obter um "Unrecognized font family" (família de fontes não reconhecidas), basta adicionar essas fontes ao destino dentro do Xcode.

Fazer isso a mão leva tempo. Por sorte, temos o rnpm, que pode ser útil. Primeiro, adicione todas as fontes na pasta res/fonts. Depois, basta declarar o rnpm no package.json e executar react-native link. Isso deverá declarar o UIAppFonts no iOS e mover todas as fontes para app/src/main/assets/fonts para o Android.

"rnpm": {
  "assets": [
    "./src/res/fonts/"
  ]
}

Acessar fontes pelo nome é propenso a erros. Podemos criar um script semelhante ao que fizemos com as imagens para gerar um acesso mais seguro. Adicione fonts.js à nossa pasta scripts:

const fs = require('fs')

const fontFileNames = () => {
  const array = fs
    .readdirSync('src/res/fonts')
    .map((file) => {
      return file.replace('.ttf', '')
    })
    
return Array.from(new Set(array))
}

const generate = () => {
  const properties = fontFileNames()
    .map((name) => {
      const key = name.replace(/\s/g, '')
      return `${key}: '${name}'`
    })
    .join(',\n  ')
    
const string = `const fonts = {
  ${properties}
}

export default fonts
`

fs.writeFileSync('src/res/fonts.js', string, 'utf8')
}

generate()

Agora, você pode usar as fontes personalizadas por meio do namespace R.

import R from 'res/R'

const styles = StyleSheet.create({
  text: {
    fontFamily: R.fonts.FireCodeNormal
  }
})

O namespace R

Essa etapa depende do gosto pessoal, mas acho mais organizada se introduzirmos o namespace R, assim como o Android faz para ativos com a classe R gerada.

Ao externalizar os recursos da sua aplicação, você pode acessá-los usando os IDs dos recursos gerados na classe R do seu projeto. Esse documento mostra como agrupar seus recursos no seu projeto para Android e fornece recursos alternativos para configurações de dispositivos específicos, além de mostrar como acessá-los a partir do código de sua aplicação ou de outros arquivos XML.

Desse modo, vamos criar um arquivo chamado R.js em src/res:

import strings from './strings'
import images from './images'
import colors from './colors'
import palette from './palette'

const R = {
  strings,
  images,
  colors,
  palette
}

export default R

Depois, vamos acessá-lo na tela:

import R from 'res/R'

render() {
  return (
    <SafeAreaView style={styles.container}>
      <Image
        style={styles.logo}
        source={R.images.logo} />
      <Image
        style={styles.image}
        source={R.images.placeholder} />
      <Text style={styles.title}>{R.strings.onboarding.welcome.title.toUpperCase()}</Text>
  )
}

Substituímos strings por R.strings, colors por R.colors e images por R.images. Com a anotação de R, fica claro que estamos acessando ativos estáticos a partir do pacote da aplicação.

Isso também corresponde à convenção da Airbnb para os singletons, já que nosso R agora é uma constante global.

23.8 Use PascalCase ao exportar um construtor/classe/singleton/função/biblioteca/objeto puro.
const AirbnbStyleGuide = {
  es6: {
  },
}

export default AirbnbStyleGuide

O que mais podemos ver?

Neste artigo, busquei mostrar como penso que deve ser a estruturação de pastas e arquivos em um projeto do React Native. Também aprendemos a gerenciar recursos e a acessá-los de maneira mais segura. Espero que você tenha achado este artigo útil. Aqui estão mais alguns recursos para explorar mais (textos em inglês):

Já que você está aqui, que tal dar uma olhada em outros artigos que escrevi (textos em inglês)?

Se gostou deste artigo, dê uma olhada também em outros artigos de minha autoria e nas minhas aplicações. Obrigado!