Today the frontend applications has become quite sophisticated. They are capable of handling complex logic to provide a seamless user experience.
With the frameworks like React/Angular/Vue the landscape of frontend has changed quite a lot. I remember the days when I used to write my application in pure HTML and use to handle the dynamic interactions with Javascript (using Jquery mostly). But today, the entire DOM is generated using Javascript. The JSX that we write the HTML template in React is a Javascript XML language that is transpiled to to Javascript which runs on the browser. And that Javascript generates the HTML.
In essence everything you see on the webpage now-a-days is generated by javascript in the background.
This gives a lot of power to developers as every part of the user interaction can now be controlled quite easily. And with the growing demand for React/Angular it is quite important to learn design patterns and clean code ethics in frontend as well. It’s not just the backend that needs to worry about it, now it’s frontend skill as well. Frontend engineer is a thing now 🙂
In this article, I will discuss about a very important design pattern that you come across all the time. It is at the heart of most of the popular frameworks. And with that design pattern, you will be able to write and run your frontend code without needing the backend to be ready. In short, we will decouple frontend development completely from the backend development, without even relying on the response from their APIs.
Now-a-days, most of the interactive applications fetches data in the background using AJAX. That makes the UI seamless and data is available when needed. So, the backend is needed only to fetch the data (most widely used JSON format). but what if you are developing a feature for which the backend is not in place yet. You can’t just sit their waiting for the backend to be ready. But you also, don’t want to make it a burden for you to re-write parts of your code when the backend is actually live.
How would you solve this problem?
If this sounds good then let’s dive in…
Table of Contents
What is Dependency Injection?
I have talked about dependency injection a few times in the past. Let me find those articles for you.
- Freedom From Redundancy Is A Trade-off
- Getting Started With Spring Framework in less than 30 minutes
In short, dependency injection allows us to inject different implementations at runtime without changing the code or using any convoluted if/else clause. It is mostly useful in scenarios where there could be multiple implementation of the same functionality depending on maybe environment or input.
A good example is something that this article is about.
Let’s say you are building a frontend application, but you need to complete the entire flow without depending on the backend service. What you could do is create a fake in your local using the localstorage
that would be used to save and retrieve the data as you want your backend APIs to perform. And then you could write another implementation that would use the service APIs to persist or retrieve the data. And these two implementations could be swapped based on the environment you are running the application.
How does it work?
Let’s understand with some diagrams.
The interaction usually happens like this:
Here, the component will have access to the Service Factory and ask for the Service Object. The factory is smart enough to know which implementation is needed to be passed based on the env the app is running. And since component doesn’t know anything about the underlying implementation and only works on the interface contract. This gives the power to change the underlying implementation at runtime.
I hope you are with me so far. If you have any question or doubt then ask in the comment box below.
This will become more clear when we will implement a small demo project.
Component Level Architecture
Before jumping in on the code, I want to explain the component level architecture. Then we will take this architecture and create components in the code. I often paint the architecture first because it helps me to understand clearly what I’m building and how different parts of my application will interact with each other.
If I have to simplify the above architecture then it would look like the following:
In the above, the View-Model interacts with service and then service is responsible for talking to the external interactions/integrations. Here, service will be provide to the View-Model based on the environment the application is running on. The design pattern that will help us achieve that is Dependency Injection.
You will see that View Model doesn’t know or care about the underlying implementation of the service object, as long as it gets the expected result.
Now, let’s jump into code and see it in action.
Demo Project Code Examples
I’ve setup a brand new React Typescript project. Following is the typical project structure:
For the sake of simplicity, I will write the code in the App.tsx
class and create some more files as we discussed in the previous section.
App.tsx
This is the pure view component. This should only contain the code that is responsible for the view of the component. There should be no logic present in this file. All logic should be present in the view model.
This is the basic component that I have created for this demo. Here we have a button that simply increments the counter when you press it.
Here is the code for it.
You will see that the component doesn’t have any logic. It’s a pure view component. All the code resides under AppViewModel
. For a moment ignore the AppViewModel and just have a look at the simplicity of this code. Its pure view (no logic).
This component simply states what needs to happen when the button is clicked, nothing more. All the logic is outsourced to the view model component that we will see later.
import React from 'react';
import './App.css';
import AppViewModel from "./App.vm";
function App() {
const vm = AppViewModel();
return (
<div className="container py-5">
<div className="row">
<div className="col">
<button className="btn btn-primary" onClick={vm.incrementCounter}>Increment Counter</button>
</div>
<div className="col">
<div className="card">
<div className="card-body">
<h2>
{vm.counter}
</h2>
</div>
</div>
</div>
</div>
</div>
);
}
export default App;
App.vm.tsx (App View Model)
This is the class that would contain all the view logic. Like processing the button click, incrementing the state and saving the state of that button. This way you would have a clear distinction between view and logic.
View and View Model has a 1-1 relation. All the logic will be carried by view model and all the view logic will be present in the view component.
Here’s the view model code:
import {useEffect, useMemo, useState} from "react";
import {IntegrationServiceProvider} from "./integration-service";
const AppViewModel = () => {
const integrationService = useMemo(() => IntegrationServiceProvider.getInstance(), []);
const [counter, setCounter] = useState(0);
const incrementCounter = async () => {
let count = counter + 1;
await integrationService.save(count);
setCounter(count);
}
useEffect(() => {
const loadCounter = async () => {
const count = await integrationService.fetch();
setCounter(count);
}
loadCounter();
}, [integrationService]);
return {incrementCounter, counter};
}
export default AppViewModel;
Here, you will see that it carries out the following operations:
- Manages the
counter'
local state - Carries out the logic on different user interactions, e.g. button click in this example
- Loads the state from the service on component load
The best part here is the IntegrationService
that provides the abstraction from the underlying service. The IntegrationServiceProvider
factory is responsible for providing the correct instance of the IntegrationService
based on the environment it is being built. This concept is highly used in the backend development but is equally powerful in frontend. I hope this is something that you take away from this article.
The beauty of this code architecture is that it is exactly the same as we discussed in the code architecture above.
Integration Service
This file contains all the code required to provide the abstract to the underlying service. This design pattern provides a powerful abstraction to your code. This provides you the power to change the entire functionality without changing a single line of code. That is the power of Dependency Injection.
The contract here becomes rather important as it controls what all functionalities will be provides to the client.
The IntegrationService
is the interface here and LocalIntegrationService
and RemoteIntegrationService
are the concrete implementations of it.
The code will make it quite easy for you to understand.
interface IntegrationService {
save: (count: number) => Promise<void>;
fetch: () => Promise<number>;
}
class LocalIntegrationService implements IntegrationService {
fetch(): Promise<number> {
let count = localStorage.getItem("counter");
if (count) {
return Promise.resolve(JSON.parse(count) as number);
}
throw Error("count not found!");
}
save(count: number): Promise<void> {
localStorage.setItem("counter", JSON.stringify(count));
return Promise.resolve();
}
}
class RemoteIntegrationService implements IntegrationService {
fetch(): Promise<number> {
throw Error("Method not implemented!");
}
save(count: number): Promise<void> {
throw Error("Method not implemented!");
}
}
class IntegrationServiceProvider {
private static instance: IntegrationService;
private constructor() {
}
static getInstance() {
if (this.instance) {
return this.instance;
}
const env = process.env.NODE_ENV;
switch (env) {
case "development":
this.instance = new LocalIntegrationService();
break;
case "production":
this.instance = new RemoteIntegrationService();
break;
default:
throw Error(`No service found for the given env: ${env}`);
}
return this.instance;
}
}
export type {
IntegrationService
}
export {
IntegrationServiceProvider
}
You can find all the code in my github repository:
Conclusion
In this article, we learned about the scalable frontend architecture and how to segregate the code such that its easier to scale and loosely coupled. We also applied many OOPS design concepts and patterns that helps the code the change and evolve easily overtime. The single responsibility principle helps in segregating the responsibility into different component such that changing one component doesn’t have any impact on any other part of the code.
Last but not least we applied the dependency injection pattern to provide abstraction from the underlying concrete service classes such that the entire functionality could be changed without changing a single line of code. This is made possible with the help of contracts that provides a loose coupling between the client and the underlying implementing class.
I hope you learned something new. If you like the article then do share it within your circle.