10 ways to better organize and design your React Application
Learn how to make your React App scale.
Intro
When building a React Application, the way you organize and design your code has a tremendous impact. It can either help you and your team find things easier, make updates quicker, and better handle the app as it grows, or make everything much worse.
It’s the same principle as with buildings. If you’ve laid the foundations and organized the building well, it can last longer and its residents will be happy. Otherwise, the building might fall.
Shaky foundations lead to shake results.
⭐ TL;DR
1. Group components by Domain Responsibilities
The organization of files and folders in a React application is crucial for maintaining clarity and manageability. The easier it is to navigate throughout the project, the less wasted time for developers to navigate and wonder where and how to change stuff.
It’s important to structure files not just by technical roles but by their domain responsibilities.
⛔ Avoid grouping components by technical responsibilities.
/src
│ ...
│
└───components
│ │ Header.js
│ │ Footer.js
│ │ ...
└───containers
│ │ InvoicesContainer.js
│ │ PaymentProfilesContainer.js
│ │ ...
└───presenters
│ │ InvoicesPresenter.js
│ │ PaymentProfilesPresenter.js
│ │ ...
│ ...
✅ Prefer grouping components by domain responsibilities by pages(routes) or modules(domains).
/src
│ ...
│
└───pages --> Actual Pages representing different parts of the app
│ └───billing
│ │ └───invoices
│ │ │ │ index.js
│ │ │ │ ...
│ │ └───payment-profiles
│ │ │ │ index.js
│ │ │ │ ...
│ │ │ ...
│ │ │ index.ts
│ └───login
│ │ index.js
│ │ ...
│ ...
2. Put components into folders
For complex components, it’s better to organize them into separate folders where you can list their subcomponents.
⛔ Avoid having a single file for each component.
/src
│
└───components
│ │ Accordion.ts
| | Alert.ts
│ │ ...
│
└───...
✅ Prefer having a single folder for each component.
/src
│
└───components
│ │ ...
│ └───accordion
│ │ │ index.ts
│ │ │ ...
│ └───alert
│ │ index.ts
│ │ types.ts
│ │ Alert.tsx
│ │ AlertTitle.tsx
│ │ Alert.stories.tsx
│ │ Alert.test.tsx
│ │ ...
│
└───...
3. Favor Absolute Paths
Using the right type of paths in your project can simplify navigation and maintenance, especially as your project grows. Refactoring will be much easier.
⛔ Avoid using relative paths which can become hard to manage and error-prone in large projects.
import { formatDate } from '../../../utils';
✅ Prefer using absolute paths which improve readability and make refactoring easier.
import { formatDate } from '@common/utils';
4. Use a common module
Common modules play a vital role in avoiding redundancy and promoting reusability across your application.
You can store utility methods, constants, components, calculations, etc. in this common module.
This centralization helps for better management and reuse.
⛔ Avoid spreading common utilities and components across various locations in your project.
✅ Prefer having a dedicated common module for all generic components and utilities used across different pages or modules.
/src
│ ...
│
└───common
│ └───components
│ │ └───dialogs
│ │ │ │ index.js
│ │ └───forms
│ │ │ │ index.js
│ │ │ ...
│ └───hooks
│ │ │ useDialog.js
│ │ │ useForm.js
│ │ │ ...
│ └───utils
│ │ │ ...
└───pages
│ └───billing
│ │ └───invoices
│ │ │ │ index.js
│ │ │ │ ...
│ ...
5. Abstract external libraries and modules
The integration of external libraries or modules requires careful consideration to ensure future flexibility and easier maintenance.
Using 3rd party libraries or components directly in your project can lead to issues if the external APIs change or you want to replace the components or library with something else. Then, you’ll have to go through all the places it is being used instead of updating them only into a single place.
Wrapping the 3rd party library or module in a custom component allows you to maintain a consistent API within your application and makes it easier to replace the module if needed in the future.
⛔ Avoid direct use of 3rd party components or libraries in your project.
// XYZ_Component.ts (file 1)
import { Button } from 'react-bootstrap';
// ABC_Component.ts (file 2)
import { Button } from 'react-bootstrap';
✅ Prefer wrapping external modules or components in a custom component.
// XYZ_Component.ts (file 1)
import { Button } from '@components/ui';
// ABC_Component.ts (file 2)
import { Button } from '@components/ui';
6. Manage dependencies between modules/pages
Managing dependencies wisely by centralizing commonly used resources in a shared common module can significantly enhance the code’s manageability and reusability.
If something is used more than once across two or more pages and modules, consider moving it to the common module.
Storing shared components or utilities in a common module eliminates the need to duplicate code across different parts of the application, making the codebase leaner and easier to maintain.
It also makes clear the dependencies per module and page.
7. Keep things as close as where they’re used (LoB)
The easier and faster for a developer to find a piece of code, the better.
The principle of Locality of Behavior (LoB) suggests organizing the codebase so that components, functions, and resources are located near where they are used within the application. The strategy promotes a modular architecture, where each part of the system is self-contained.
This improves readability and maintainability. When developers work on a feature, they have all related code in proximity which makes it easier to understand and modify. It also reduces the cognitive load of tracing through distant files and modules.
/src
│ ...
│
└───common
│ └───components
│ │ │ ...
│ └───hooks
│ │ │ ...
│ └───utils
│ │ │ ...
└───pages
│ └───billing
│ │ └───invoices
│ │ │ │ index.js
│ │ │ │ ...
│ │ └───payment-profiles
│ │ │ │ index.js
│ │ │ │ ...
│ │ └───hooks
│ │ │ │ index.js
│ │ │ │ useInvoices.js
│ │ │ │ usePaymentProfiles.js
│ │ │ │ ...
│ │ └───utils
│ │ │ │ index.js
│ │ │ │ formatAmount.js
│ │ │ │ ...
│ ...
8. Be careful with utility functions
Utility functions typically handle reusable snippets of code that aren’t tied to the business rules or the application logic. They rather provide general assistance across the system.
Utility functions should remain pure and purpose-specific, focusing on general tasks like formatting dates, converting data types, etc. Mixing them with business logic like how data is processed or business-specific decisions, can make these utilities overly complex and less reusable.
Also, business logic and rules change more often than utilities, so by separating them you will improve the overall maintainability of the code.
⛔ Avoid adding business logic to utils.
✅ Prefer extracting business logic into separate functions.
9. Be careful with business logic
Integrating business logic directly into UI components can lead to problems like harder-to-test code and poor separation of concerns. This can also lead to bulky components that are difficult to manage and update.
In React, custom hooks are a great tool for abstracting business logic from components. By using hooks, you can encapsulate business logic and keep your UI clean and focused on rendering.
This separation not only makes your components more modular and easier to manage but also enhances reusability and maintainability.
⛔ Avoid mixing business logic with UI.
✅ Prefer separating business logic from UI. Use custom hooks.
10. Pin Dependencies
When managing a JavaScript project, your package.json file plays a crucial role. It defines your project’s dependencies - the external packages your project relies on to function. Pinning these dependencies refers to specifying exact versions of these packages rather than allowing version ranges.
Pinning dependencies ensures that everyone working on the projects, as well as the production environment, uses the exact same version of each package, eliminating discrepancies that might occur due to minor updates or patches.
⛔ Avoid using version ranges in your package.json.
{
"dependencies": {
"express": "^4.17.1",
"react": ">=16.8.0"
}
}
✅ Prefer using exact versions in your package.json
.
{
"dependencies": {
"express": "4.17.1",
"react": "16.8.0"
}
}
📣 Articles worth reading
How Stripe Prevents Double Payment Using Idempotent API by
How to say "No" and win back your time as a software engineer by
How to propose an impactful improvement to the codebase and own the implementation by
"20% for tech debt" doesn't work by
9 Things I Wish I Knew When I Started Programming by
🗞️ You may also like
✌️Get in touch
You can find me on LinkedIn or Twitter.
This newsletter is funded by paid subscriptions from readers like yourself.
If you aren’t already, consider becoming a paid subscriber to receive the full experience!
Think of it as buying me a coffee twice a month, with the bonus that you also get all my products for FREE.
You can also hit the like ❤️ button at the bottom of this email to help support me or share this with a friend to get referral rewards. It helps me a ton!
I like it, and agree with most of it. One point I differ on is dependencies. My experience is that pinning them makes the eventual upgrade too difficult. I favor regular maintenance and updating of the deps. I think of them as an organism that needs regular care.
Awesome article Petar, congrats on 10k subs!