Building UIs without worrying about API data format.
This Article uses React Native for Demonstration but the concepts work with any UI framework or simple HTML.
Code can be found at the end of the article.
If you’re a Frontend Developer chances are that you’ve been slowed down by waiting for the API.
What if I told you that your UI doesn’t need to depend upon whatever the API data format looks like ?
Let me tell you how it’s done.
Let’s say you have to make a component as follows :
If you observe this Component, you only need 5 different information:
1. Name
2. Phone Number
3. Gender
4. Age
5. Image
However, when backend decides to send data for this component, it might include extra information like : id, address e.t.c.
This component doesn’t care about any other data expect the described 5.
So, we can create a Model for those 5 data as :
export default interface UserCardViewMOdel {
name: string,
phone: string,
gender: string,
age: string,
image: string,
}
Then,
We can have the component take that data as a prop.
const UserCard = ({data}: {data: UserCardViewModel}) => {
return (
...
----- Component UI defined here -----
...
)
}
With this, we have all the data the UserCard
component needs in order to render.
The required data can be accessed from the data
variable as :
const UserCard = ({data}: {data: UserCardViewModel}) => {
return (
...
<Text>{data.name}</Text>
...
)
}
Now,
The UserCard
component doesn’t care about anything that happens with the API. This component is able to render the UI as long as it is given data
of type UserCardViewModel
.
This makes the component you write reusable across multiple projects as you can copy the component along with it’s View Model
when using it. The term View Model
comes from MVVM
which is for Model, View, View Model
.
A Model
is a data class which holds the data of your app. A View Model
is just a Model
but for a View
. A View
is anything that appears on screen.
Now,
Let’s say that the data provided by the backend looked as :
export default interface UserResponse {
firstName: string,
lastName: string,
phone: string,
gender: string,
dob: string,
image: string,
}
You’ll realize that the backend data is completely different from what you need in your UI. To make our component render we need the data to be of type UserCardViewModel
.
For this, we have to convert UserResponse
into UserCardViewModel
.
This can be done in various ways.
# The Common Way
One way is to convert it while passing it to the component inside the UI as :
const HomeView = () => {
let response = useGetResponseSomehow();
return (
...
<UserCard data={{
name: `${response.firstName} ${response.lastName}`,
phone: response.phone,
gender: response.gender,
age: getAgeFromDob(response.dob),
image: response.image,
}} />
...
)
}
This will work, but, you’ve cluttered your UI code with data conversion logic.
So, a better way is to separate out this logic into an adapter.
# The Better Way
You create an adapter as follows :
export const UserCardAdapter = {
convert(response: UserResponse): UserCardViewModel {
return {
name: `${response.firstName} ${response.lastName}`,
phone: response.phone,
gender: response.gender,
age: getAgeFromDob(response.dob),
image: response.image,
};
}
}
We can have the HotelCardAdapter
be a Singleton.
Then, use it as :
const HomeView = () => {
let response = useGetResponseSomehow();
return (
...
<UserCard data={UserCardAdapter.convert(response)} />
...
)
}
This is good.
But, this means that you need to remember to write this .convert()
statement for every component.
So, we can take this adapter idea one step furthur by abstracting it.
# The More Better Way
The abstraction for the adapter can be created as :
export default interface Adapter<T,D> {
convert(data: T): D;
}
Then,
Every adapter can implement the interface. In our UserCardAdapter
example, it will become :
export const UserCardAdapter : Adater<UserResponse, UserCardViewModel> = {
convert(response: UserResponse): UserCardViewModel {
return {
name: `${response.firstName} ${response.lastName}`,
phone: response.phone,
gender: response.gender,
age: getAgeFromDob(response.dob),
image: response.image,
};
}
}
Now,
We can create an adapter component that will call the .convert()
as :
const AdaptedComponent = <T,D> ({
data,
adapter,
children,
}: {
data: T,
adapter: Adapter<T,D>
children: (data: D) => JSX.Element
}) => {
const adaptedData = adapter.convert(data);
return children(adaptedData);
}
export default AdaptedComponent;
Then, this can be used as :
const HomeView = () => {
let response = useGetResponseSomehow();
return (
...
<AdaptedComponent<UserResponse, UserCardViewModel>
adapter={UserCardAdapter}
data={response}
>
{
data => <UserCard data={UserCardAdapter.convert(response)} />
}
</AdaptedComponent>
...
)
}
Why is this good even though clearly, I seem to have to write more code then remembering to use .convert()
?
It’s good because, we have abstracted the logic of .convert()
and all we need to do is wrap our component with <AdaptedComponent />
.
With this, we’ll not have to worry about our API’s data format.
All we need to do is create a Model for our GUI and then adapt the data from API into the GUI.