best practices

Application Structure

Absolute Paths

Different parts of an application should be as easily modifiable as possible. This applies not only to the component code itself but also to its location. Absolute paths ensure that you don't need to change anything when moving an imported component to another location. It also simplifies determining the component's location.

Example

Relative Path:

Code implementation
1import Button from '../../../components/ui/button';
2

Absolute Path:

Code implementation
1import Button from '@/components/ui/button';
2

Here, "@" is used as an indicator of an internal module, though the tilde "~" symbol is also encountered.

One Component — One Directory

Effectively organizing a project involves adhering to the principle of "One Component — One Directory". Each component in the application should have its own directory, ensuring better scalability and maintainability of the code. Begin by creating the component itself, and if additional files are needed, such as styles or tests, they should also reside within this directory.

A good practice is to include an index.ts file for re-exporting the component. This significantly reduces import paths and eliminates redundancy in component names. For instance, the following example demonstrates what to avoid:

Code implementation
1import Navbar from '@/components/widgets/Navbar';
2

However, it's not recommended to place the component's code directly into the index.ts file, as this complicates searching for the component by name in the code editor. Instead, use the index.ts file for named exports to create a cleaner import structure and follow best practices for re-exporting components.

For example, the index.ts file for the Navbar component would look like this:

Code implementation
1import Navbar from './Navbar';
2
3export { Navbar };
4

This approach ensures that your imports remain simple and consistent throughout the project.

Having established the importance of organizing components in their own directories and using index.ts for cleaner imports, let's take a look at how this principle can be applied in practice.

Inefficient file organization — placing all files in one location:

Code implementation
1├── components
2 ├── widgets
3 ├── Navbar.tsx
4 ├── Navbar.css
5 ├── Navbar.test.tsx
6 ├── Sidebar.tsx
7 ├── Sidebar.css
8 ├── Sidebar.test.tsx
9

In this example, all widget-related files are placed in the same directory, which can make it harder to manage as the project grows.

Efficient file organization — placing files in their own directories:

Code implementation
1├── components
2 ├── widgets
3 ├── navbar
4 ├── index.ts
5 ├── Navbar.tsx
6 ├── Navbar.css
7 ├── Navbar.test.tsx
8 ├── sidebar
9 ├── index.ts
10 ├── Sidebar.tsx
11 ├── Sidebar.css
12 ├── Sidebar.test.tsx
13

This structure improves navigation and management of components, making the project more understandable and maintainable. By keeping each component and its related files in a dedicated directory, you streamline the development process, simplify maintenance, and enhance overall project scalability.

Wrapping External Components

It's advisable to avoid directly importing a large number of external components. An effective solution is to create adapters for such components, allowing their API to be modified if necessary. This also simplifies replacing used libraries in one place without needing to change the code throughout the project.

This approach applies to both component libraries and various utilities. One of the simple and effective methods is to re-export such components from a common module. This makes the project more flexible and reduces dependency on specific libraries.

Example

Inefficient approach — direct import of external components:

Code implementation
1import { DropDown } from 'pseudo-library-1';
2import { Modal } from 'pseudo-library-2';
3

Efficient approach — importing external components from an internal module:

Code implementation
1import { DropDown, Modal } from '@/modules/adapters';
2

Efficiently wrapping external components through adapters enhances code support and testing, as components aren't tied to specific libraries and their changes.

Separation of Business Logic and UI Components

Components such as buttons and input fields are widely used throughout applications. Separating business logic components from UI components also involves organizing file structures accordingly. This facilitates easier navigation within the project and promotes component reuse.

The project structure should clearly distinguish between components designed for business logic and those for UI. Consider an example file structure of a project:

Code implementation
1├── components
2 ├── ui
3 ├── button
4 ├── index.ts
5 ├── Button.tsx
6 ├── Button.css
7 ├── input
8 ├── index.ts
9 ├── Input.tsx
10 ├── Input.css
11 ├── containers
12 ├── user-form
13 ├── index.ts
14 ├── UserFormContainer.tsx
15 ├── UserFormContainer.test.tsx
16 ├── product-list
17 ├── index.ts
18 ├── ProductListContainer.tsx
19 ├── ProductListContainer.test.tsx
20

In this example, UI-related components are located in the "ui" folder, while components containing business logic are placed in the "containers" folder. Thus, the business logic related to user data input is handled within the UserFormContainer component, while the Input and Button components remain focused on rendering the UI and delegating interactions to the container. Similarly, the same UI components, such as Input and Button, are used in the ProductListContainer, with the business logic encapsulated within this respective container.

This separation helps maintain clean and easily maintainable code, allowing for easy modification and independent testing of components.

Last updated on by @skrykateSuggest an improvement on Github repository.