You've probably built some great native apps with React Native, but now it's time to scale it. For that, you'll need a solid project structure. How do you organize your files and folders in a rapidly growing application? How do you ensure consistency and uniformity in your project's structure? In this guide, we'll talk about some of the best practices you can adopt for your React Native project structure.
What Does a Project Structure Constitute?
First, let's make sure you understand what a project structure actually is.
In a nutshell, a project structure represents how your files, folders, and directories are organized inside a project. It also comprises what type of files your project contains and how you split your modules into sub-modules, larger directories into smaller ones, etc.
Let's jump in and see the initial project structure we get with a new React Native app.
First, we'll create an Expo-based project. So, inside a directory of your choice, run the following command:
expo init rn-project-structure
Great! Now, when we open this project, Expo would have already created an initial set of files and folders for us. This actually represents the initial structure of our projects. Here's what that looks like:
In the root directory, App.js is the file that kicks off the project. Then there's an assets folder where static assets can be placed. Finally, there are some config and dependency management files like app.json, babel.config.js,package.json, etc.
We usually don't take the above initial structure into account when we talk about project structure. However, the initial structure gives us an idea as to which type of files may be best put in the root directory.
Break Down the Project
Now let's talk about some common project structures you can implement no matter what technology you're using.
You can always break your front-end app into three layers:
UI or Presentation Layer. Represents all the components or UI elements the user interacts with like buttons, popups, text, etc.
Logic Layer. Responsible for maintaining your core business logic. It's also responsible for all the events and managing the interactions with the presentation layer.
API Layer. Responsible for all the back-end interactions. This is where your app makes API calls to a database server or an external web service.
Hence, at the simplest, you can always group these into separate folders for each. Take a look at the following directory structure:
Also, notice how the file has an extension, api.js to indicate that it's an API file. This is the simplest project structure you can achieve. It can be useful for small projects like personal projects, POC projects, or small MVPs for your startup.
Type-Based Project Structure
In most scenarios, the previous folder structure will not be sufficient. You'll have to narrow it down further to fit practical projects. So now let's discuss the type-based project structure where we segregate our files and folders based on their type.
Here's what a type-based project structure looks like:
We've added the following folders:
components. Contains all files and folders for reusable components.
constants. Includes constant copy texts or strings like placeholders, UI texts, screen names, event names, etc.
pages/screens. Contains different pages or screens of your app like HomeScreen, LoginScreen, etc.
wrappers. High-level wrappers or layers over existing UI or functions. This may include higher-order components or providers for some special usage like analytics, event tracking, etc.
For more context, here's a more refined version of the type-based project structure:
Notice how the example files and folders inside each subdirectory pertain to the type of that subdirectory. For instance, inside the hooks directory, there's a useAuth hook. Since hooks in React begin with the use keyword, we know that useAuth is a custom hook for handling the authentication state. Henceforth it is rightfully placed in the hooks directory.
One of the great benefits of the type-based project structure is reusability. You can always translate this structure to any other front-end app you might build. For example, you might build a desktop app with Electron. You can carry forward this type of project structure to reuse some of the files and folders directly.
The constants, utils, and api folders can be directly used in any other similar project you want to build. Also, there's more abstraction between different layers of your application. This gives you separation of concerns while building your app.
Feature-Based Project Structure
Previously, we categorized our files and folders based on their type. However, there's another way to go about this. You can have your project structure based on the features it implements. This is more suited for projects that release tons of features every now and then, maybe a startup that's trying to reach a product-market fit. Since they're iterating rapidly, it makes more sense to have a feature-based project structure.
Note that this project structure is based on the modified version of the type-based structure. There's still some high-level segregation base on the type. To demonstrate that, let's look at the following example:
Notice how most of our type-based project is still in place. However, we've narrowed down the constants, hooks, helpers, ui, and so on to different features.
So, we have a directory called feature at the root of our project. Based on the feature type, we have subdirectories inside it. For instance, authentication in itself is a big feature, so it has a subdirectory underneath. Then further inside the authentication or Auth folder, we have folders for Login, Signup, etc.
Then for each of these features, we've separated the interface or template code, constants for placeholder texts, custom hooks for managing state updates, etc.
This type of project structure provides a more in-depth idea about the features you're building. It's more developer-friendly for quick iteration and speeding up the workflows. However, it may not be scalable since, for huge codebases, there are going to be so many features that you'll run into decision fatigue when deciding how to differentiate them.
Atoms, Molecules, and Organisms
You've seen two types of project structures. But as your project grows larger, its UI and template components could very well grow out of hand. Hence, a lot of times you may want to further break down your UI or template components.
Consider the following breakdown of your UI/templates:
Let's discuss what we've done in the above case.
First, you'll break your UI into the smallest low-level native wrappers. These are custom UI wrappers around native components. For instance, you could have a generic button component. It will be a higher-order component and will conditionally render the TouchableOpacity, TouchableHighlight, or Button component using props.
After this point, you'll never want to directly use the above native components to build UIs. You'll use your custom button component.
Next, you'll use your atoms to create molecules! In other words, you'll use your custom button component to create different buttons. Maybe you'll have a different button for your login form and another button for redirecting to the signup form.
These components may or may not be completely reusable. But they should be rendered reusable on at least more than one page of your application.
Finally, you'll use your molecules or molecules and atoms to build organisms! You'll use the above components to build screens and pages.
For instance, you can use your login form and login buttons to create the entire login page. Your organisms are not intended for reusability, but if you can reuse them, kudos to you!
Incorporating Third-Party Libraries
Your React Native project will have big, critical dependencies. This aligns with the React ecosystem. So how should you structure it?
Let's consider the most common scenario where you'll use dependencies:
Context API/Redux: Global store and state management
Here's how you can organize them within your existing project structure:
We have a separate directory for navigation. This contains all your files and folders pertaining to in-app navigation. For instance, it has a screens folder that may render different screen wrappers for different screens.
Then you could group your screens inside the stack or the tab navigator. You could also have a constants file for mapping the name of your screens with their headers, names you send in events, etc.
For state management, we have the store, actions, and reducers folders. We then have separate files inside each. Notice how this segregation uses a feature-based project structure. For instance, we have separate actions for login and cart. Similarly, we have reducers and global states/stores for each.
Remember how you saw earlier that we had a providers subdirectory inside the hoc folder? You can add your Redux or Context API wrappers there.
Where Should Your Tests Go?
Finally, the billion-dollar question: Where should your tests go? There are two schools of thought here.
First, you could place your test files pertaining to each component or feature inside that component's or feature's directory.
Or you could have a separate _tests_ directory inside the root of your project. Here you can initiate a completely new tests structure. Something along the following lines:
If you write intensive tests for each feature and component, go for the first approach. If you write more generalized tests and cover different types of tests, go for the second. Further, if you use something like Waldo for running automated tests in your browser, you might not even need to think about tests in your project structure!
Project structures are more important than you think. Give it enough thought so that you don't have to refactor your structures as your React Native codebase demands it. Feel free to use a combination of any of the structures we've discussed, depending on the scale, complexity, and type of your projects. Until next time!