My favourite React (Native) project architecture

I have seen and used lots of different architectures for a React (Native) project, but I prefer the modular one which evolved a little in my hands, as of January 2022. Here is a quick overview.

I have seen and used lots of different architectures for a React (Native) project, but I prefer the modular one which evolved a little in my hands, as of January 2022. Here is a quick overview.

Based on experience

Throughout the years, I have seen many architectures and had various React projects, built multiple apps, big and small. Most of them used pretty standard, e.g.Β based on routes, file types or features. But from the experience, the modular architecture has served me very well.

Overview

πŸ’‘Note: my approach works for both React and React Native projects, but there are little differences.

πŸ’‘Note: I will be using the β€˜structure’ and β€˜architecture’ words interchangeably for simplicity.

Of course, nowadays, the architecture and file structure is rather dictated by the stack and technologies used. For example, Gatsby.js or Next.js propose their own way of doing a React project. Redux gives distinction between Container (smart) and Presentational (dumb) components. That is totally fine too, but with this, the structure is not universal enough, especially if you want to migrate to React Hooks, for example.

For a bare React or React Native project, the modular architecture works well because the project becomes very testable, composable, without any extra complexity. So there is no learning curve for it. You can get it, just opening the repository. It is a good one because it also becomes easy to maintain and develop new features. And advanced techniques can play well too - e.g.Β module splitting. For instance, if the user is not logged in, there is no need to load and parse all available modules which are not reachable without logging in.

My vision of the architecture is a bit customised and adapted for even greater universality and integrity. And what is important is that it is based on continuous iterations.

Let’s take a look at such a project, it is a React Native project, but it works nice for web projects too, taking into account the platform differences, of course. There are some considerations underneath. Look (1), (2) etc. for footnotes.

I have been using TypeScript for all projects by default, so the file extensions are in accordance.
β”œβ”€β”€ .circleci (CI configuration to run test and lint or similar folders)
β”œβ”€β”€ .github
| β”œβ”€β”€ PULL_REQUEST_TEMPLATE.md
β”œβ”€β”€ assets (fonts and global images)
β”œβ”€β”€ android (native parts of the app)
β”œβ”€β”€ ios (native parts of the app)
β”œβ”€β”€ node_modules
β”œβ”€β”€ scripts (scripts for development, CI, downloading/generating GraphQL schemas)
| β”œβ”€β”€ downloadSchema.ts
β”œβ”€β”€ src
| β”œβ”€β”€ __generated__ (1)
| | β”œβ”€β”€ localModels.tsx
| | β”œβ”€β”€ remoteModels.tsx
| β”œβ”€β”€ jest (test specific files and helpers) (2)
| β”œβ”€β”€ components (basic ui atoms and molecules, can be used throughout the app) (3)
| | β”œβ”€β”€ Button
| | | β”œβ”€β”€ index.tsx (entry file for the component)
| | | β”œβ”€β”€ Button.test.tsx (test file should have the folder name)
| | | β”œβ”€β”€ Button.styles.[css|ts] (styles can be extracted, optional)
| | β”œβ”€β”€ TextField
| | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ TextField.test.tsx
| | | β”œβ”€β”€ TextField.styles.[css|ts]
| | β”œβ”€β”€ index.ts (entry file for easier re-exporting, optional)
| β”œβ”€β”€ hocs (universal higher order components)
| | β”œβ”€β”€ withPermissions.ts
| | β”œβ”€β”€ withPermissions.test.ts
| β”œβ”€β”€ hooks (custom React Hooks)
| | β”œβ”€β”€ useAsyncFnOnMount.ts
| | β”œβ”€β”€ useAsyncFnOnMount.test.ts
| β”œβ”€β”€ context (React Context modules and their providers)
| | β”œβ”€β”€ PermissionsContext.ts (or both under Permissions/)
| | β”œβ”€β”€ PermissionsProvider.ts
| β”œβ”€β”€ redux (4)
| β”œβ”€β”€ modules
| | β”œβ”€β”€ common
| | | β”œβ”€β”€ screens
| | | | β”œβ”€β”€ CommonLoadingScreen.tsx
| | β”œβ”€β”€ auth
| | | β”œβ”€β”€ components (local components used only for the current module)
| | | | β”œβ”€β”€ AuthBiggerButton.tsx
| | | β”œβ”€β”€ screens
| | | | β”œβ”€β”€ AuthSignInScreen.tsx
| | | | β”œβ”€β”€ AuthSignUpScreen.tsx
| | | | β”œβ”€β”€ AuthForgotPasswordScreen (in case of multiple files, can be a folder)
| | | | | β”œβ”€β”€ index.tsx
| | | | | β”œβ”€β”€ AuthForgotPasswordScreen.test.tsx
| | | β”œβ”€β”€ ... (other things like specific queries, very local helpers, .etc)
| | β”œβ”€β”€ dashboard
| | β”œβ”€β”€ profile
| | β”œβ”€β”€ developer (module for debugging purposes, screens hidden in Production)
| β”œβ”€β”€ navigation (navigation setup and utils)
| | β”œβ”€β”€ components (navigation components, used in the navigators)
| | β”œβ”€β”€ navigators
| | | β”œβ”€β”€ AuthStackNavigator
| | | | β”œβ”€β”€ AuthRoute.ts (routes for the Auth module and types for screens)
| | | | β”œβ”€β”€ index.tsx (Auth navigator)
| | | β”œβ”€β”€ DashboardStackNavigator
| | | | β”œβ”€β”€ DashboardRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ ProfileStackNavigator
| | | | β”œβ”€β”€ ProfileRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ DeveloperStackNavigator
| | | | β”œβ”€β”€ ProfileRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ ModalStackNavigator
| | | | β”œβ”€β”€ ModalRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ RootStackNavigator
| | | | β”œβ”€β”€ RootRoute.ts (app-level routes)
| | | | β”œβ”€β”€ index.tsx (main navigator)
| β”œβ”€β”€ services (SDKs, wrappers and abstractions of services)
| | β”œβ”€β”€ graphql (all graphql queries should be extracted into here) (5)
| | | β”œβ”€β”€ remote (schema, queries and mutations for backend communication)
| | | | β”œβ”€β”€ fragments
| | | | β”œβ”€β”€ mutations
| | | | β”œβ”€β”€ queries
| | | | β”œβ”€β”€ schema.graphql
| | | β”œβ”€β”€ local (local schema, queries and mutations)
| | | β”œβ”€β”€ index.ts (client)
| | β”œβ”€β”€ EventTracking
| | | β”œβ”€β”€ EventTracking.test.ts
| | | β”œβ”€β”€ index.ts (main or entry file)
| | β”œβ”€β”€ GeolocationService
| | | β”œβ”€β”€ GeolocationService.test.ts
| | | β”œβ”€β”€ index.ts
| | β”œβ”€β”€ Sentry.ts
| | β”œβ”€β”€ Stripe.ts
| | β”œβ”€β”€ EventListener.ts
| β”œβ”€β”€ international
| | β”œβ”€β”€ locales
| | | β”œβ”€β”€ en.json
| | | β”œβ”€β”€ pl.json
| | | β”œβ”€β”€ index.ts (common import of all locales, optional)
| | β”œβ”€β”€ index.ts (setup file, e.g. i18n)
| β”œβ”€β”€ typings (TypeScript declaration files for packages missing them)
| | β”œβ”€β”€ react-native-config.d.ts
| β”œβ”€β”€ utils (helpers and utils used across the modules, should be thoroughly tested) (6)
| | β”œβ”€β”€ constants.ts (AsyncStorage keys, URLs, etc.)
| | β”œβ”€β”€ authUtils.ts
| | β”œβ”€β”€ authUtils.test.ts
| | β”œβ”€β”€ systemUtils.ts
| | β”œβ”€β”€ systemUtils.test.ts
| | β”œβ”€β”€ stringUtils.ts
| | β”œβ”€β”€ stringUtils.test.ts
| | β”œβ”€β”€ transformSearchResultsIntoList.ts
| | β”œβ”€β”€ transformSearchResultsIntoList.test.ts
| | β”œβ”€β”€ index.ts (common import of all utils, optional)
| β”œβ”€β”€ index.tsx (the root component of the app)
β”œβ”€β”€ .env.development (development environment constants)
β”œβ”€β”€ .env.staging (staging environment constants)
β”œβ”€β”€ .env.production (production environment constants)
β”œβ”€β”€ graphql.codegen.yml (GraphQL code generation setup)
β”œβ”€β”€ index.js (main entry file)
β”œβ”€β”€ package.json (dependencies, Jest setup, other options)
β”œβ”€β”€ babel.config.js
β”œβ”€β”€ jest.setup.js (Adapter setup and importing mocks, polifills)
β”œβ”€β”€ jest.config.js
β”œβ”€β”€ react-native.setup.js
β”œβ”€β”€ metro.config.js
β”œβ”€β”€ lint-staged.config.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ .prettierrc.js
β”œβ”€β”€ .prettierignore
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ yarn.lock
β”œβ”€β”€ .circleci (CI configuration to run test and lint or similar folders)
β”œβ”€β”€ .github
| β”œβ”€β”€ PULL_REQUEST_TEMPLATE.md
β”œβ”€β”€ assets (fonts and global images)
β”œβ”€β”€ android (native parts of the app)
β”œβ”€β”€ ios (native parts of the app)
β”œβ”€β”€ node_modules
β”œβ”€β”€ scripts (scripts for development, CI, downloading/generating GraphQL schemas)
| β”œβ”€β”€ downloadSchema.ts
β”œβ”€β”€ src
| β”œβ”€β”€ __generated__ (1)
| | β”œβ”€β”€ localModels.tsx
| | β”œβ”€β”€ remoteModels.tsx
| β”œβ”€β”€ jest (test specific files and helpers) (2)
| β”œβ”€β”€ components (basic ui atoms and molecules, can be used throughout the app) (3)
| | β”œβ”€β”€ Button
| | | β”œβ”€β”€ index.tsx (entry file for the component)
| | | β”œβ”€β”€ Button.test.tsx (test file should have the folder name)
| | | β”œβ”€β”€ Button.styles.[css|ts] (styles can be extracted, optional)
| | β”œβ”€β”€ TextField
| | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ TextField.test.tsx
| | | β”œβ”€β”€ TextField.styles.[css|ts]
| | β”œβ”€β”€ index.ts (entry file for easier re-exporting, optional)
| β”œβ”€β”€ hocs (universal higher order components)
| | β”œβ”€β”€ withPermissions.ts
| | β”œβ”€β”€ withPermissions.test.ts
| β”œβ”€β”€ hooks (custom React Hooks)
| | β”œβ”€β”€ useAsyncFnOnMount.ts
| | β”œβ”€β”€ useAsyncFnOnMount.test.ts
| β”œβ”€β”€ context (React Context modules and their providers)
| | β”œβ”€β”€ PermissionsContext.ts (or both under Permissions/)
| | β”œβ”€β”€ PermissionsProvider.ts
| β”œβ”€β”€ redux (4)
| β”œβ”€β”€ modules
| | β”œβ”€β”€ common
| | | β”œβ”€β”€ screens
| | | | β”œβ”€β”€ CommonLoadingScreen.tsx
| | β”œβ”€β”€ auth
| | | β”œβ”€β”€ components (local components used only for the current module)
| | | | β”œβ”€β”€ AuthBiggerButton.tsx
| | | β”œβ”€β”€ screens
| | | | β”œβ”€β”€ AuthSignInScreen.tsx
| | | | β”œβ”€β”€ AuthSignUpScreen.tsx
| | | | β”œβ”€β”€ AuthForgotPasswordScreen (in case of multiple files, can be a folder)
| | | | | β”œβ”€β”€ index.tsx
| | | | | β”œβ”€β”€ AuthForgotPasswordScreen.test.tsx
| | | β”œβ”€β”€ ... (other things like specific queries, very local helpers, .etc)
| | β”œβ”€β”€ dashboard
| | β”œβ”€β”€ profile
| | β”œβ”€β”€ developer (module for debugging purposes, screens hidden in Production)
| β”œβ”€β”€ navigation (navigation setup and utils)
| | β”œβ”€β”€ components (navigation components, used in the navigators)
| | β”œβ”€β”€ navigators
| | | β”œβ”€β”€ AuthStackNavigator
| | | | β”œβ”€β”€ AuthRoute.ts (routes for the Auth module and types for screens)
| | | | β”œβ”€β”€ index.tsx (Auth navigator)
| | | β”œβ”€β”€ DashboardStackNavigator
| | | | β”œβ”€β”€ DashboardRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ ProfileStackNavigator
| | | | β”œβ”€β”€ ProfileRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ DeveloperStackNavigator
| | | | β”œβ”€β”€ ProfileRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ ModalStackNavigator
| | | | β”œβ”€β”€ ModalRoute.ts
| | | | β”œβ”€β”€ index.tsx
| | | β”œβ”€β”€ RootStackNavigator
| | | | β”œβ”€β”€ RootRoute.ts (app-level routes)
| | | | β”œβ”€β”€ index.tsx (main navigator)
| β”œβ”€β”€ services (SDKs, wrappers and abstractions of services)
| | β”œβ”€β”€ graphql (all graphql queries should be extracted into here) (5)
| | | β”œβ”€β”€ remote (schema, queries and mutations for backend communication)
| | | | β”œβ”€β”€ fragments
| | | | β”œβ”€β”€ mutations
| | | | β”œβ”€β”€ queries
| | | | β”œβ”€β”€ schema.graphql
| | | β”œβ”€β”€ local (local schema, queries and mutations)
| | | β”œβ”€β”€ index.ts (client)
| | β”œβ”€β”€ EventTracking
| | | β”œβ”€β”€ EventTracking.test.ts
| | | β”œβ”€β”€ index.ts (main or entry file)
| | β”œβ”€β”€ GeolocationService
| | | β”œβ”€β”€ GeolocationService.test.ts
| | | β”œβ”€β”€ index.ts
| | β”œβ”€β”€ Sentry.ts
| | β”œβ”€β”€ Stripe.ts
| | β”œβ”€β”€ EventListener.ts
| β”œβ”€β”€ international
| | β”œβ”€β”€ locales
| | | β”œβ”€β”€ en.json
| | | β”œβ”€β”€ pl.json
| | | β”œβ”€β”€ index.ts (common import of all locales, optional)
| | β”œβ”€β”€ index.ts (setup file, e.g. i18n)
| β”œβ”€β”€ typings (TypeScript declaration files for packages missing them)
| | β”œβ”€β”€ react-native-config.d.ts
| β”œβ”€β”€ utils (helpers and utils used across the modules, should be thoroughly tested) (6)
| | β”œβ”€β”€ constants.ts (AsyncStorage keys, URLs, etc.)
| | β”œβ”€β”€ authUtils.ts
| | β”œβ”€β”€ authUtils.test.ts
| | β”œβ”€β”€ systemUtils.ts
| | β”œβ”€β”€ systemUtils.test.ts
| | β”œβ”€β”€ stringUtils.ts
| | β”œβ”€β”€ stringUtils.test.ts
| | β”œβ”€β”€ transformSearchResultsIntoList.ts
| | β”œβ”€β”€ transformSearchResultsIntoList.test.ts
| | β”œβ”€β”€ index.ts (common import of all utils, optional)
| β”œβ”€β”€ index.tsx (the root component of the app)
β”œβ”€β”€ .env.development (development environment constants)
β”œβ”€β”€ .env.staging (staging environment constants)
β”œβ”€β”€ .env.production (production environment constants)
β”œβ”€β”€ graphql.codegen.yml (GraphQL code generation setup)
β”œβ”€β”€ index.js (main entry file)
β”œβ”€β”€ package.json (dependencies, Jest setup, other options)
β”œβ”€β”€ babel.config.js
β”œβ”€β”€ jest.setup.js (Adapter setup and importing mocks, polifills)
β”œβ”€β”€ jest.config.js
β”œβ”€β”€ react-native.setup.js
β”œβ”€β”€ metro.config.js
β”œβ”€β”€ lint-staged.config.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ .prettierrc.js
β”œβ”€β”€ .prettierignore
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .eslintrc.js
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ yarn.lock
  1. The __generated__/ folder can be used for Relay or GraphQL generated types, for example.
  2. The jest can also be used for __fixtures__/ or test helpers. Mock implementations should ideally be placed here in __mocks__/, in the same folder at the node_modules level.
  3. The components/ folder can optionally have the ui/ folder which should contain all the universal components and some other folders for more complex components, for example Form which is also universal but consists of smaller components. This is very similar to atoms/molecules/organisms principle.
  4. There are many ways to structure Redux specific files, for example, based on modules or based actions/reducers. Ideally this folder structure should follow principles, mentioned in this article, but also refer to Redux docs for details.
  5. GraphQL allows defining multiple schemas and use them in the app, that is why remote/local separation is helpful. The remote schema is used for backend API and the local one can complement the remote one and can be used for typing and saving search filters or combined types, for example. So next time when implementing a type/model which, let’s say, consists of 2 partial backend types, consider using the local schema for that. Together with GraphQL codegen, it is a very powerful tool. A few examples:
type DashboardSearchFilterOutputType {
showOnlyOpenRestaurants: Boolean!
locationId: Int
}

enum ThemeColorType {
DARK
LIGHT
}

enum ModuleSource {
COMMON
AUTH
DASHBOARD
PROFILE
DEVELOPER
}
type DashboardSearchFilterOutputType {
showOnlyOpenRestaurants: Boolean!
locationId: Int
}

enum ThemeColorType {
DARK
LIGHT
}

enum ModuleSource {
COMMON
AUTH
DASHBOARD
PROFILE
DEVELOPER
}

6. The utils/ folder can be structured freely to fit your needs. If a helper is a big one, consider a dedicated file for it. If the helpers take just a few lines, they can go all in one relevant file.

Considerations

This also shows common naming for files. Ideally, each folder should contain the entry file, usually called index.ts. Other files’ names in the folder should start with the module name. It helps to read the code even better. When importing, strive to preserve the full name.

If you find yourself using a module-specific component in different places/modules throughout the app, it should be made universal and moved to common components then.

Each module might not have the entry file because all screens are typically of equal importance, that is totally fine.

If the project has some screens which are shared and used in different modules, for example LoadingScreen, consider creating a module called common/.

For my latest projects, I avoid using Redux altogether because it increases complexity exponentially. React Hooks or Context are a very concise and expressive way, so Container components become redundant.

Conclusion

It goes without saying, the structure might differ depending on specific needs of the app. Feel free to extend or adapt it even further to fit your app needs. If in doubt, refer to one of the following JavaScript style guides or libraries’ best practices.

When you introduce a convention or a style guide rule, it is more important to follow it consistently than the rule itself. Each team member should stick to it after the rule has been introduced. Later refactoring would be a dream in this case.