05 Mar 2022 ~ 3 min read

Building a Scalable and Modular React-TS Architecture

React.js is largely unopinionated, apart from some of the design patterns that are recommended and enforced while using hooks. And as a result, there is no official recommendation for organising your project and this most often times could lead to an architectural quagmire of components.

After experimenting with several different patterns and architectures at our current company, we finally ended up in a modular architecture that enabled us to scale our projects effortlessly.

Core Principles

Isolation of Modules

Every module needs to be isolated with its own dependencies and should be aware of all the dependencies it requires with an entry point for all its APIs.

No-circular dependency

Modules should not import each other as it breaks the Acyclic-dependencies principle.

Differentiation between Modules

Dependent vs Independent modules. Independent modules are not so tightly coupled with other modules apart from common modules within the application, enabling us to out-source it to an external library.

Modular Architecture for React Applications

Dependent Modules

The modular architecture diagram represents a simple scalable modular architecture that adheres with our core principles.

Note: Every arrowhead in the diagram above represents an import statement.

Application

The application/root module is simply the collection/composition of containers(a.k.a smart components). It's responsible for bootstrapping the application with react-suspense, error-boundaries and any top-level providers. In general, it should contain as very minimal code as possible.

Containers

The containers module is a collection of smart components that are connected to the state via the hooks. As you can see that the containers modules only import common, components and hooks modules and it doesn’t concern the state module at all. This helps us to scale the hooks module without affecting the states.

Hooks

The hooks module supplies data to the containers through the state and also updates the state based on the user interaction with the container components.

State

The state module is responsible for populating the client-side state using the service and common modules. This enables us to avoid polluting the containers and hooks with service APIs.

Independent Modules (Possibly)

Independent modules are only coupled to the common modules, although this is not as independent as we would like it to be, it's still easier to outsource it to an external library.

Components

The components module is just a collection of purely functional components that do not care about the application state.

Although this could be purely independent like an external library, you might still end up building application-specific components that closely resemble the data types from the common module. Hence it's often helpful to isolate/mark generic and specific components. Generic components do not use any types from the common modules, thereby enabling us to move them to an external component library.

Services

Services modules form the collection of APIs that interact with the backend and the resulting response should align with the core data types present in our common modules.

Conclusion

Each module represented in the diagram could be a folder of its own in your react-project, with an entry point(eg: index.ts).

And if done correctly every bubble could grow in size without affecting the other bubble.