The path to a scalable and maintainable React frontend architecture
Modern Frontend Architecture — 101
A few weeks ago my colleagues were curious about our front-end architecture and project structure. After a few small presentations, I figured it would be a good idea to wrap it all up and share my approach here.
Motivation
Modern frameworks and libraries make it easy for us to focus on reusability when developing UI components. This is a very important concept for developing maintainable front-end applications. Over the course of many projects, I have found that it is often not enough to just build reusable components. Applications could become unmaintainable if requirements changed significantly or new requirements came up.
It took more and more time to find the right files or to debug something.
Sure, I can train my memory skills or learn to use Visual Studio Code even better. But I’m mostly part of a team, so I’m not the only one working on the code. So we need to better the set up of our projects for everyone. That means we should make them more maintainable and more scalable. The aim is to make changes to current functionalities as fast as possible, but also to be able to develop new features more quickly.
The view from the top
As a full stack developer, I originally come from the backend side.
There one can find many architectural approaches that we can make use of. Two widely used concepts are “Domain-Driven Development” and “Separation of Concerns”.
They are quite useful in the front-end architecture as well.
“DDD” is an approach in which one tries to create groups that are technically related. With “SoC” we separate the responsibilities and dependencies of logic, UI and data from one another.
The frontend architecture base
When a user interacts with our web application, the app routes them to the correct module. Each module contains certain functions and business logic. In addition, each module has the option of communicating with the backend using the application layer (http / s) through an API (REST, GraphQL).
The React Project-Setup
If we want to set up our React project structure, we can rely on the structure shown below. The entire code for the application is located in the app directory. Each module, which mostly corresponds to a route, is located in the modules folder.
Reusable UI components, most of which do not contain any special logic, are located in the components directory.
All configurations such as redux middleware, axios for requests or constants can be found in the config folder. In services one finds business logic functions that are used by modules. Model interfaces, support for several languages, global redux reducers or helper functions that are not particularly specific are located in the shared directory. The JSONs for several languages but also text strings can be found in the i18n folder.
app/
> config/
> components/
> services/
> modules/
> shared/
content/
> css/
> img/
> fonts/
> js/
cypress/
i18n/
webpack/
.gitignore
package.json
tsconfig.json
Readme.md
The content directory contains our assets (e.g. images, fonts or 3rd party JS code). One should always use a testing framework (here Cypress), which is in its own directory.
Webpack is used as an asset bundler. The remaining .json files are configurations. Of course we don’t want to forget our readme.
Let’s check out the details
With the basic architecture and project structure, we have a good foundation. Now we need more details to implement the frontend architecture.
It’s always helpful to create a component architecture.
I like to use draw.io for this.
There we name the individual components and modules.
This part is very important, because here we see the hierarchy and usage of the components. I try to visually separate components and module-specific components that have been used several times (dashed vs. normal line). It is also a good idea to add middleware or additional functionalities here.
Modules are large UI components with logic. Other modules can use components, but not other modules. If different modules want to communicate with each other, we use Redux. Each module consists of the main component, a logic file and a reducer file. Our entire business and UI logic can be found in the hook for the logic. In the reducer we can place the Redux implementation and all API calls.
app/
> modules/
> shop/
> shop.tsx
> shop.logic.ts
> shop.reducer.ts
By using the hook standards in React, we outsource the entire functionality to the logic and then use this hook in our module component.
The component in the shop module could then look like this.
//app/modules/shop/shop.tsx
const Shop = (props) => {
const { state, actions, reducer } = useShopLogic(props); return (
<div id="shop">
<Header />
{!state?.loading &&
<Button onClick={actions?.buyNow}>
<Translate contentKey="shop.buyNow"/>
</Button>}
<FeaturedProducts products={reducer?.products} />
<Footer />
</div>
);
};
export default Shop;
Now let’s look at an example of the hook. This contains the entire code that is relevant for the interaction with the UI, as well as communicating with the API via Reducer / Redux.
//app/modules/shop/shop.logic.ts
export const useShopLogic = (props) => {
const dispatch = useDispatch();
const history = useHistory();
const [loading, setLoading] = useState(false);
const { products } = useSelector(({shop}) => {
products: shop?.products;
});
const buyNow = () => {
history.push("/product/1");
};
const loadProducts = () => {
setLoading(true);
dispatch(ShopReducer.loadProducts());
};
useEffect(() => {
if (ProductService.areValid(products)) setLoading(false);
}, [products]);
useEffect(() => {
loadProducts();
}, []);
return {
state: { loading },
actions: { buyNow },
reducer: { products },
};
};
As one can clearly see here, the responsibility between UI definition and functionality has been separated. This enables the logic to be reused and the interface to be easily adapted.
The Shop Reducer handles the global state logic through Redux.
Our API functions are also located here, which can then be called from anywhere.
//app/modules/shop/shop.reducer.tsexport
const ACTION_TYPES = {
FETCH_PRODUCTS: "shop/FETCH_PRODUCTS",
RESET: "shop/RESET",
};
const initialState = {
products: null,
loading: false,
success: false,
};
export type ShopState = Readonly<typeof initialState>;
export default (state: ShopState = initialState, action): ShopState => {
switch (action.type) {
case PENDING(ACTION_TYPES.FETCH_PRODUCTS):
return {
...state,
success: false,
loading: true,
};
case FAILURE(ACTION_TYPES.FETCH_PRODUCTS):
return {
...state,
success: false,
loading: false,
};
case SUCCESS(ACTION_TYPES.FETCH_PRODUCTS):
return {
...state,
products: action.payload.data,
success: true,
loading: false,
};case ACTION_TYPES.RESET:
return {
...initialState,
};
default:
return state;
}
};
export const getProducts = () => async (dispatch) => {
const requestUrl = `${SERVER_API_URL}${API_MAP.getProducts}`;const result = await dispatch({
type: ACTION_TYPES.FETCH_PRODUCTS,
payload: axios.get < any > requestUrl,
});return result;
};
export const reset = () => ({
type: ACTION_TYPES.RESET,
});
I think it makes less sense to store the API calls in the component. API requests are often used in several places and in different modules, so I prefer to keep them separate from the business logic. With the right middleware, the reducers are very modular and require little code. The handling of requests can also be solved by a central reducer, which ultimately leaves almost only the API call functions.
Summary
In this article I wanted to show what a frontend architecture can look like. The frontend is the first entry point for our users.
Since our applications are constantly adapting and growing, they become more prone to errors. But bugs prevent products from being released and new features are expected to be created even faster.
This is simply not possible without some sort of structure and order.
With a good frontend architecture however, we can create a stable foundation with which we can tackle these challenges much better.
For enthusiasts seeking to refine their React architecture for scalability and maintainability, the insights offered in this comprehensive guide https://elitex.systems/blog/using-react-for-front-end-development-a-full-guide/ are invaluable. It complements the discussed concepts with practical advice on leveraging React’s full potential, ensuring your frontend development is not only robust but also future-proof. A must-read for anyone committed to mastering React’s ecosystem.
I have read your blog on build a scalable and maintainable PHP application architecture. It was very interesting and helpful but I can add some extra points in your article. Here some extra points:
1.Don’t use vertical scaling.
2.Do favor horizontal scaling.
3.Don’t default to physical servers.
These are some extra points to add to your article. Readers if you are confused about your web and App development , you can get a free consultation at Alakmalak technologies.Visit our site for more information.